Skip to content

Release v2.4.0 - #351

Merged
Minitour merged 70 commits into
mainfrom
develop
Oct 23, 2026
Merged

Minitour merged 70 commits into
mainfrom
develop

Conversation

@Minitour

@Minitour Minitour commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

Summary

Release v2.4.0:

  • capa status reports the current project as well as the server.
  • Plugin fixes: GitLab marketplaces, exec-form hooks, hooks that read CLAUDE_PLUGIN_ROOT, and skills with the same name from two plugins.
  • Safety: capa clean and stored secrets are safer.
  • Search mode: clearer errors.
  • Also: a JSON Schema for capabilities.yaml, and capa upgrade works on Windows.

Closes #305, closes #306, closes #308, closes #309, closes #310, closes #312, closes #320, closes #321, closes #324, closes #325, closes #326, closes #330, closes #332, closes #345, closes #349.

What changed

New

Fixed

Security

Docs

Maintenance

Upgrade notes

  • Run capa install once in each project after upgrading. This:
    • records the install, so capa status can track changes; until then a project shows "Out of date: installed by an older capa that did not record installs";
    • re-copies plugins installed by earlier versions, so plugin skills that are symlinks resolve;
    • rewrites plugin hooks with their args and CLAUDE_PLUGIN_ROOT.
  • Plugin skills with the same name: the second one is now installed as /<plugin id>-<skill> instead of being skipped or overwritten. To make another plugin's version the plain name, list it first in capabilities.yaml.
  • Git refs: a version or ref that git itself would reject (for example HEAD~1 or main@{1}) now fails with an error naming the field.
  • Update notice: "A new version of capa is available" now goes to stderr instead of stdout.

Screenshots / logs

N/A (see the individual PRs).

Test plan

Checklist

  • Tests added or updated
  • bunx tsc --noEmit passes
  • bun run smells clean vs base (or CI Qlty Smells job green)
  • Docs updated (if user-facing)

🤖 Generated with Claude Code

dependabot Bot and others added 30 commits October 12, 2026 15:47
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.38.2 to 4.38.3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@v4.38.2...v4.38.3)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.38.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
…lop/github/codeql-action-4.38.3

chore(deps): bump github/codeql-action from 4.38.2 to 4.38.3
capa upgrade runs install.ps1 with powershell.exe, which is always
Windows PowerShell 5.1. 5.1 reads a BOM-less script as Windows-1252, so
the UTF-8 check mark and box-drawing glyphs decoded into stray smart
quotes and the script failed to parse. It never ran a single line.
The failure was invisible because the installer was spawned detached
with stdout/stderr ignored.

- install.ps1 is ASCII-only now; `e (a 7.0-only escape) becomes [char]27
- run the Windows installer in the foreground with inherited stdio and
  a real exit code, same as install.sh; the installer already renames
  the running capa.exe aside, so capa doesn't need to exit first
- test that install.ps1 stays ASCII-only
fix(upgrade): make capa upgrade work on Windows
The builder kept the generator from the existing lockfile, so adding a
skill after upgrading capa left an old version in capabilities.lock.
Restamp generator together with generatedAt when the pins change; a
no-op install still leaves the file untouched.

Closes #310
Two bugs kept plugins from gitlab-org/ai/skills from installing:

- "./.claude-plugin/plugins/glab" lost its leading dot. The parser
  stripped "./" and the path join stripped a leading "." again, so
  capa looked for "claude-plugin/plugins/glab". Only a leading "./"
  (or a bare ".") is stripped now, in the generic marketplace parser
  and the bundled claude-plugins adapter (pin updated).
- Each plugin's skills/<name> is a symlink to the repo's top-level
  skills/. Skill discovery skipped symlinked dirs, and the stable plugin
  copy kept the relative link, which broke once copied. Discovery now
  accepts symlinked skill dirs, and the plugin copy goes through
  copySkillTree, which dereferences symlinks that resolve inside the
  repo snapshot and still rejects ones that point outside it.
  copySkillTree also stops silently dropping symlinks and guards
  against link cycles.

Closes #312
The at-rest migration re-encrypted any enc:v1 value it couldn't decrypt.
A process started with the wrong master key (restored or swapped
master.key, different HOME, key-store bug) re-wrapped every stored
secret under that key, so restoring the right key later couldn't
recover them.

Values that don't decrypt are now left byte-identical. The count is
reported: a warning when the server starts, secretStorage.undecryptable
in /health, and a line in capa status. Only plaintext is migrated.

Closes #305
Hook entries were upserted by event and matcher, so changing a hook's
`on` or `matcher` added a new capa:<id> entry and left the old one
running. The database locator then pointed only at the new entry, so
neither install nor capa clean removed the old one.

Before placing an entry, capa now drops entries tagged with the same
capa:<id> in other events (and, for Claude-like shapes, other matcher
groups). An entry already in the target group stays where it is, so
re-installs don't reorder the file. Antigravity is keyed by hook name
and was not affected.

resolveMatcher also returns "" instead of undefined when a mapping
carries an explicit undefined matcherPrefix.

Closes #306
Skill, plugin and hook entries pass def.version, def.ref (and repo
suffixes) to `git rev-parse --verify <ref>^{commit}`. Nothing checked
them first, so rev-parse's revision syntax (HEAD~2, main@{1}, :/text,
a..b) and option-like values reached git.

resolveRef, the only place these values reach git, now accepts only
plain tag/branch names and SHAs (letters, digits, . _ / + -, no leading
"-", no "..") and fails with a clear error otherwise. Lockfile pins
are checked the same way.

Closes #308
The capabilities-manager reference still described ${PLUGIN_DATA} as
~/.capa/plugin-data/<projectId>/<pluginId>/ that survives plugin updates.
Since #286 the folder is <slug>-<hash>, keyed by the declared source, and
it is removed when the plugin is no longer declared and by capa clean.
Match docs/README.md.

Closes #309
Review follow-ups for the symlink support in this PR:

- copySkillTree walks the real tree first, then follows symlinks, and
  enters each real directory once per walk. The per-chain cycle guard
  still allowed links pointing at each other to copy 2^N trees (a hang
  and a full disk for capa install and the server's re-resolve); a link
  to a directory already copied is now skipped, and a real directory
  always lands at its own path.
- The plugin copy skips broken links and links leaving the repository
  with an install warning instead of failing the whole plugin, as cpSync
  used to copy them. Other copySkillTree callers still throw.
- The stable-copy stamp is versioned (copy-v2) so plugins copied by the
  old cpSync path, with broken relative links, are copied again once.
- The marketplace plugin preview checks the subpath with
  assertSafeRepoPath, like install does.
removeTaggedElsewhere deleted every empty event array in the hooks
object, including ones it never touched (a user's "Stop": [] or a
Gemini disabled: []). Only delete an array this call emptied.
The allowlist rejected tags git accepts, such as changesets/lerna
monorepo tags (@scope/pkg@1.2.3), "feature#12" or unicode names, which
installed fine before. Reject what git check-ref-format rejects instead
(control characters, space, ~ ^ : ? * [ \, "..", "@{", a leading
"-", "/" or ".", and similar), which still rules out rev-parse's
revision syntax and option-like values.

The offline snapshot fast path only checked that a lockfile pin was 40
characters long before using it as a path segment, so a tampered
resolvedRef could point at another local directory. It now requires 40
hex characters; anything else falls through to resolveRef, which
rejects it.
In search mode agents gave up on capa when a call failed:

- An unknown call_tool name said "Make sure you've called setup_tools to
  activate the required skills", which doesn't exist in search mode and
  reads as misconfiguration. Unknown names now get one message on both
  call paths: not in the catalog, hidden by a server's expose
  except/exactly policy when that's the reason, the closest names (a bare
  remote name like get_issue maps to github.get_issue, which is how
  server descriptions name tools), and the next step for the exposure
  mode. It still starts with "Tool not found".
- A tool that exists but isn't activated now says to call
  search("<exact name>"), which activates it.
- Searching a tool's exact name (qualified, local id or remote name) ranks
  it first instead of among every tool sharing a word; a name-like query
  with no exact match says the tool isn't exposed by this project.

Closes #321
The variables UI, capa configure and install --env scanned every string
in the effective capabilities for ${…}. Tools synthesized from a
server's expose policy carry the remote server's own descriptions, so
prose such as Grafana's ${var:regex} showed up as a required project
variable. Inline skill bodies and descriptions had the same problem.

extractRequiredVariables reads only the fields capa substitutes: MCP
server defs (resolveMcpServerDef) and command tool init/run cmd, dir
and env (tool-executor). Plugin-sourced entries stay excluded.

Closes #320
tool-executor resolves the whole command spec, so a ${...} in an
init/run argument default is substituted at runtime. Include defaults
when collecting required variables; argument descriptions stay prose.
Qodo review follow-ups for unknownToolMessage:

- Servers with hand-written tools: entries have their expose policy
  turned off (serversWithExposePolicy semantics), so it is never named
  as the cause.
- A bare name that is callable on another server (public.shared) is
  suggested instead of being reported as blocked by one server's
  except list.
- Only a name listed under an active expose: except is reported as
  hidden; the list proves the tool exists. Under exactly, and now none,
  a qualified name might just be a typo, so the policy is added as a
  note next to the normal not-found answer and suggestions.
fix(lockfile): stamp the current capa version when pins change
…-312

fix(registries): resolve GitLab marketplace plugins
…only

fix(variables): only config fields can make a variable required
fix(mcp): clearer search-mode errors and exact-name search
docs(skills): correct the PLUGIN_DATA path and lifetime
fix(cache): validate git version/ref values before rev-parse
fix(hooks): remove the old entry when a hook event or matcher changes
fix(secrets): never re-encrypt secrets that fail to decrypt
After a passthrough install no providers are on record, so capa clean
fell back to every provider, including OpenClaw, whose skills dir is the
project's own skills/. It then removed skills/<id> for every declared
skill, deleting committed type: local sources.

- Skill dirs are only removed for providers the project resolved; with
  none on record, only providers whose skills dir is their own dot
  directory (.claude/skills, .cursor/skills, ...) are swept, which still
  cleans up after a passthrough install.
- A skill dir that is, contains or sits inside a type: local skill's
  source is never removed, whatever the provider.

Closes #324
Claude Code plugin hooks can use exec form: { command: "node", args: [...] },
spawned without a shell. parseHookAction never read args, so capa
installed a bare `node`, which Claude Code fed the hook JSON on stdin and
which failed with a SyntaxError on every matching event.

- args are parsed, carried on UnifiedHookEntry and Hook, kept by
  validateHooks, and ${CLAUDE_PLUGIN_ROOT} is expanded in each one.
- Claude Code (supportsExecArgs) gets the args array as is.
- Providers that only take a command string get the args appended
  POSIX-quoted, so each stays one verbatim argument.
- The plugin hook merge key includes args, so `node a.js` and
  `node b.js` are not coalesced.

Closes #325
Some plugin hooks read the CLAUDE_PLUGIN_ROOT environment variable at
runtime (ponytail: node -e "...process.env.CLAUDE_PLUGIN_ROOT...").
Claude Code only sets it for hooks of an installed plugin, not for hooks
in project settings, so once capa wrote them there they threw a
TypeError on every prompt. This affected server mode and --passthrough
alike; the report saw a difference only because an older ponytail used
the ${CLAUDE_PLUGIN_ROOT} placeholder, which capa already expands.

${CLAUDE_PLUGIN_ROOT} placeholders are expanded first, so any name left
is a runtime read: capa now prefixes CLAUDE_PLUGIN_ROOT=<plugin copy> to
the command. An exec-form hook that reads it becomes one shell command,
since exec form can't set environment variables.

Closes #326
Minitour and others added 3 commits October 23, 2026 01:00
The project API blanked every MCP server env value, so the UI showed empty
fields even though spawn resolved them. Stored defs keep `${VAR}`
placeholders (resolved only at spawn), so those are returned as-is, along
with plain literals. A literal is still blanked when its name reads like a
credential or it embeds URL credentials.

Fixes #349
Address Qodo review on #350: the name check now covers PASS and PAT as a
segment (DB_PASS, GITHUB_PAT_VALUE), and the value itself is checked for
Bearer/Basic, well-known token prefixes and long token-shaped strings, so a
secret under a generic name stays blank. Paths and plain config still show.
fix: show MCP env placeholders and plain literals in the project API
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (1) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Search hides why a tool is unavailable ✓ Resolved
Description
Search-mode misses mark every name-like query with no exact exposed match as not exposed, even
when the name is simply unknown or belongs to a tool hidden by the exposure policy. The search
response therefore omits the policy-specific recovery step and conflates unknown, hidden, and
unactivated tools for agents.
Code

src/server/mcp-handler.ts[R702-706]

+			const payload = buildSearchPayload(query, matches, {
+				unknownName:
+					isToolNameQuery(query) &&
+					exactNameMatches(candidates, query).length === 0,
+			});
Evidence
The search handler sets unknownName solely from exact matches in the exposed candidate list, while
the payload unconditionally describes such queries as not exposed. The separate policy-aware logic
exists for direct tool-call errors but is not applied to search-mode responses, violating the
requirement to distinguish unknown, hidden, and unactivated tools and provide the appropriate
recovery step.

Provide actionable search-mode tool errors
src/server/mcp-handler.ts[699-706]
src/server/mcp-tool-defaults.ts[141-152]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Search-mode queries set `unknownName` only by checking the already-filtered exposed candidates, so unknown tools and tools hidden by an exposure policy receive the same `not exposed` message without policy-specific recovery guidance.
## Fix Focus Areas
- src/server/mcp-handler.ts[699-706]
- src/server/mcp-tool-defaults.ts[136-152]
## Recommended Fix
Retain enough capability and exposure-policy context when building search responses to distinguish genuinely unknown names from hidden tools, and provide the appropriate next step for each exposure mode while preserving the existing activation guidance for known but inactive tools.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Invalid pins can reach git 📎 Requirement gap ⛨ Security
Description
resolveRef validates pinnedSha only as a generic Git ref, while the snapshot fast path merely
skips non-40-character values instead of rejecting them. A short SHA or another valid ref therefore
reaches Git and can be resolved and used in snapshot handling instead of failing as an invalid
lockfile pin.
Code

src/shared/cache/snapshot.ts[R131-132]

+		// A lockfile pin becomes a path segment here, so it must be a real SHA.
+		const fullSha = /^[0-9a-f]{40}$/i.test(opts.pinnedSha) ? opts.pinnedSha : null;
Evidence
The changed snapshot logic uses the 40-hex test only to select an offline cache path, and the
changed resolver validates pinnedSha with the generic Git ref-name validator. Non-SHA pins
consequently continue to Git resolution, contrary to the requirement that lockfile pins be real SHAs
before they are used in snapshot handling.

Validate git versions and refs while parsing capabilities
src/shared/cache/snapshot.ts[131-135]
src/shared/cache/mirror.ts[303-305]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Lockfile pins are required to be real commit SHAs, but the new snapshot check only recognizes full SHAs for the offline fast path and does not reject other values before `resolveRef` passes them to Git.
## Fix Focus Areas
- src/shared/cache/snapshot.ts[131-132]
- src/shared/cache/mirror.ts[303-305]
## Recommended Fix
Validate every lockfile `pinnedSha` against the full hexadecimal commit SHA format before ref resolution or path construction, and raise a clear error for short SHAs, branch names, or other non-SHA values while retaining the broader Git ref validation for `version` and `ref`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Renamed skills vanish from plugin agents ✓ Resolved
Description
resolvePlugins records only a conflicted skill's new installed ID in pluginSkillIds, but later
filters the plugin agent's original manifest skill IDs against that set. When an agent declares a
skill renamed because another skill owns its name, the generated subagent drops that skill from its
skills list.
Code

src/cli/commands/plugin-install.ts[R782-784]

+      skillOwners.set(skillId, pluginInstallId);
+
+      pluginSkillIds.push(skillId);
Evidence
The conflict branch chooses and stores a new ID, while agent metadata contains manifest skill IDs
and the subsequent filter accepts only stored IDs.

src/cli/commands/plugin-install.ts[763-786]
src/cli/commands/plugin-install.ts[876-901]
src/types/plugin.ts[138-151]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A conflict rename changes a plugin skill's installed ID without changing references to that skill in the plugin's agent metadata.
## Fix Focus Areas
- src/cli/commands/plugin-install.ts[763-786]
- src/cli/commands/plugin-install.ts[876-901]
## Recommended Fix
Track each plugin skill's original ID and installed ID, then translate that plugin's agent skill references through the mapping before creating its subagents. Cover an agent referencing a skill renamed after a collision.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Some legacy secrets stay in plaintext ✓ Resolved
Description
canonicalizeStoredSecret treats every value beginning with enc:v1: as ciphertext and now leaves
it unchanged whenever decryption fails. If a legacy plaintext secret happens to start with that
prefix, migration leaves it unencrypted in the database and counts it as undecryptable instead.
Code

src/shared/secret-crypto.ts[R60-63]

decryptCiphertext(value);
-		return value;
+		return { value, undecryptable: false };
} catch {
-		return encryptSecret(value);
+		return { value, undecryptable: true };
Evidence
Failed decryption of any prefixed value now returns the original value; migration updates a row only
when that value changes. The read path likewise returns the original value after decryption failure.

src/shared/secret-crypto.ts[46-76]
src/db/migrate-secrets.ts[11-31]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Legacy plaintext beginning with the ciphertext prefix is preserved rather than encrypted, even when it cannot be a valid stored ciphertext.
## Fix Focus Areas
- src/shared/secret-crypto.ts[46-65]
- src/db/migrate-secrets.ts[11-20]
## Recommended Fix
Validate the ciphertext envelope before treating a prefixed value as potentially encrypted. Encrypt clearly malformed prefixed legacy values while preserving well-formed ciphertext that fails authentication under the current master key, and test both cases.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. A failed plugin still takes a skill name from the next plugin ✓ Resolved
Description
skillOwners.set(skillId, pluginInstallId) records a plugin's claim to a skill name. The plugin's
catch rollback truncates mergedSkills and rebuilds the server, hook, sub-agent and rule
registries, but it never removes that plugin's entries from skillOwners. If a plugin that comes
first fails after claiming /handoff, a later plugin that also ships handoff is renamed to
/-handoff. Its warning then names the failed plugin as the owner, so no plugin ends up with the
plain /handoff name.
Code

src/cli/commands/plugin-install.ts[782]

+      skillOwners.set(skillId, pluginInstallId);
Evidence
The collision lookup treats any skillOwners entry as a taken name (lines 766-770), and the claim
is recorded at line 782. The failure rollback at lines 1026-1054 resets the other merge state but
leaves skillOwners alone, so the next plugin still sees the stale owner.

src/cli/commands/plugin-install.ts[766-782]
src/cli/commands/plugin-install.ts[1026-1054]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
When a plugin fails partway through install, its claims in `skillOwners` stay in place. Later plugins are then renamed (`<plugin>-<skill>`) even though the failed plugin contributed no skill.
## Fix Focus Areas
- src/cli/commands/plugin-install.ts[766-782]
- src/cli/commands/plugin-install.ts[1026-1054]
## Recommended Fix
In the per-plugin `catch` rollback, delete every `skillOwners` entry whose value is `pluginInstallId`. Use `for (const [id, owner] of skillOwners) if (owner === pluginInstallId) skillOwners.delete(id);`. Add a test where the first plugin fails after claiming a skill and check that the second plugin keeps the plain name.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (1)
6. Credentials in project server settings now reach the UI ✓ Resolved
Description
redactEnvValue now returns a literal environment value unchanged when neither its key-name nor
value heuristic matches, whereas redactServerForApi previously blanked every literal. Generic
names and ordinary-looking secrets—including short values, URL query passwords, token-only URL
userinfo, and base64 tokens containing / or +—can therefore pass through to the project-details
or capabilities/configure API response.
Code

src/server/secret-redaction.ts[R70-77]

+function redactEnvValue(name: string, value: string): string {
+	const literal = value.replace(PLACEHOLDER, "").trim();
+	if (!literal) return value;
+	if (SENSITIVE_ENV.test(name)) return "";
+	if (CREDENTIAL_VALUE.some((re) => re.test(value) || re.test(literal))) {
+		return "";
+	}
+	return value;
Evidence
The key-name pattern does not match generic names such as CONFIG, DATABASE_URL,
SENTRY_ENDPOINT, or WEBHOOK_URL; the userinfo pattern requires user:pass@, and the long-token
pattern rejects /. For values that match none of these heuristics, the new fallback returns the
original literal instead of the earlier unconditional blank, and the project-details route passes
configured server environment values through that redactor before returning them.

src/server/secret-redaction.ts[43-78]
src/server/secret-redaction.ts[94-101]
src/server/secret-redaction.ts[43-77]
src/server/project-routes.ts[204-214]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Literal server environment values with generic names can pass through `redactEnvValue` unchanged and reach project-facing API responses, including when a secret appears in a URL or a base64 token.
## Fix Focus Areas
- src/server/secret-redaction.ts[43-78]
- src/server/secret-redaction.ts[94-100]
- src/server/project-routes.ts[204-214]
## Recommended Fix
Redact configured literal environment values by default while preserving visibility for placeholders or values identifiable as references without exposing a literal secret. If selective masking is retained, cover URLs with any userinfo (`://[^/\s@]+@`) or a query string (`?`), allow `/`, `+`, and `=` in the long-token check, and recognize `url`, `dsn`, `uri`, and `conn` in key names. Add tests for a generic key with a short secret, `DATABASE_URL=postgres://h/db?password=x`, and an AWS-style secret containing `/`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can reply 'qodo' on any finding to push back, ask questions, or dig deeper

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/server/mcp-handler.ts
Comment on lines +131 to +132
// A lockfile pin becomes a path segment here, so it must be a real SHA.
const fullSha = /^[0-9a-f]{40}$/i.test(opts.pinnedSha) ? opts.pinnedSha : null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Invalid pins can reach git 📎 Requirement gap ⛨ Security

resolveRef validates pinnedSha only as a generic Git ref, while the snapshot fast path merely
skips non-40-character values instead of rejecting them. A short SHA or another valid ref therefore
reaches Git and can be resolved and used in snapshot handling instead of failing as an invalid
lockfile pin.
Agent Prompt
## Issue description
Lockfile pins are required to be real commit SHAs, but the new snapshot check only recognizes full SHAs for the offline fast path and does not reject other values before `resolveRef` passes them to Git.

## Fix Focus Areas
- src/shared/cache/snapshot.ts[131-132]
- src/shared/cache/mirror.ts[303-305]

## Recommended Fix
Validate every lockfile `pinnedSha` against the full hexadecimal commit SHA format before ref resolution or path construction, and raise a clear error for short SHAs, branch names, or other non-SHA values while retaining the broader Git ref validation for `version` and `ref`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/cli/commands/plugin-install.ts
Comment thread src/shared/secret-crypto.ts
Comment thread src/cli/commands/plugin-install.ts
Comment thread src/server/secret-redaction.ts
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Release v2.4.0: project status, plugin fixes, and safety improvements

✨ Enhancement 🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Adds read-only project reporting to capa status, backed by recorded install outcomes.
• Fixes plugin skills, hooks, marketplace paths, and symlink handling.
• Protects local skills and stored secrets while improving search, schema support, and Windows
 upgrades.
Diagram

graph TD
  Install["capa install"] --> Fingerprint["Install fingerprint"] --> DB[("Install state DB")]
  Status["capa status"] --> DB
  Status --> Health["Server health"]
  Install --> Plugins["Plugin installer"] --> Copy["Safe skill copy"]
  Plugins --> Hooks["Hook handlers"]
  MCP["MCP handler"] --> Search["Tool search"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Fingerprint local skill sources as well
  • ➕ Detects local skill changes after installation.
  • ➖ Makes status more expensive and the fingerprint more complex.

Recommendation: The install-record approach supports useful read-only status without changing the existing install flow. Its fingerprint covers the capabilities file and lockfile pins, but not local skill contents; consider extending it if status must detect that kind of drift.

Files changed (74) +4202 / -226

Enhancement (22) +2452 / -65
capabilities.schema.jsonPublish generated capabilities JSON Schema +1676/-0

Publish generated capabilities JSON Schema

• Adds a Draft-07 schema for editor autocomplete and validation.

schema/capabilities.schema.json

context.tsTrack install failure messages +2/-0

Track install failure messages

• Adds a failures list to the install context for status reporting.

src/cli/commands/install-tasks/context.ts

install-error-policy.tsRecord install failures in either error mode +2/-0

Record install failures in either error mode

• Collects messages from immediate and batched install failures.

src/cli/commands/install-tasks/install-error-policy.ts

install.tsPersist project install outcomes +77/-1

Persist project install outcomes

• Records fingerprints, counts, completion, and redacted failure messages for 'capa status'. Captures task errors in continue-on-error mode.

src/cli/commands/install.ts

status.tsReport server and project status +273/-45

Report server and project status

• Adds read-only project states, install comparisons, problems, JSON output, and a strict exit status. Also reports undecryptable secrets from server health.

src/cli/commands/status.ts

index.tsExpose status flags and keep JSON stdout clean +8/-5

Expose status flags and keep JSON stdout clean

• Registers '--json' and '--strict' and moves upgrade notices to stderr.

src/cli/index.ts

tasks.tsReport continued task errors to callers +6/-1

Report continued task errors to callers

• Adds an onError callback for tasks that throw when exit-on-error is disabled.

src/cli/ui/tasks.ts

database.tsExpose install state and undecryptable secret count +12/-1

Expose install state and undecryptable secret count

• Adds project install-state accessors and stores the secret migration's undecryptable count.

src/db/database.ts

projects.tsStore and read project install records +85/-1

Store and read project install records

• Adds install-state upsert and read helpers, including a read-only-compatible reader and deletion with the project.

src/db/projects.ts

schema.tsAdd project_install_state table +14/-0

Add project_install_state table

• Stores the last install fingerprint, capa version, outcome, errors, and item counts.

src/db/schema.ts

index.tsSurface undecryptable stored secrets +10/-1

Surface undecryptable stored secrets

• Logs a startup warning and includes the count in the health response.

src/server/index.ts

mcp-handler.tsGive actionable unknown-tool errors +82/-5

Give actionable unknown-tool errors

• Suggests closest tool names, explains applicable exposure policies, and distinguishes missing tools from tools not yet activated in search mode.

src/server/mcp-handler.ts

mcp-tool-defaults.tsClarify unmatched tool-name searches +8/-2

Clarify unmatched tool-name searches

• Marks a tool-name query without an exact catalog match as not exposed.

src/server/mcp-tool-defaults.ts

capabilities.tsGenerate editor schema from capabilities validation +55/-3

Generate editor schema from capabilities validation

• Builds the JSON Schema, accepts editor-only '$schema' metadata, and writes schema references into new capabilities files.

src/shared/capabilities.ts

config.tsRead the database path without creating settings +17/-0

Read the database path without creating settings

• Adds a path lookup for read-only commands that does not create capa configuration.

src/shared/config.ts

install-fingerprint.tsFingerprint installed capabilities and lockfile pins +50/-0

Fingerprint installed capabilities and lockfile pins

• Hashes normalized capabilities text and semantic lockfile content, and identifies which changed.

src/shared/install-fingerprint.ts

claude-code.tsEnable exec-form hooks for Claude Code +2/-0

Enable exec-form hooks for Claude Code

• Marks Claude Code as supporting command and args arrays.

src/shared/providers/entries/claude-code.ts

tool-search.tsPrioritize exact tool names and suggest matches +46/-0

Prioritize exact tool names and suggest matches

• Adds exact qualified, local, and remote name matching plus suggestions for unknown names.

src/shared/tool-search.ts

database.tsType recorded project install outcomes +14/-0

Type recorded project install outcomes

• Adds the ProjectInstallState interface for fingerprints, results, errors, and counts.

src/types/database.ts

hooks.tsType exec-form hook arguments +6/-0

Type exec-form hook arguments

• Adds an optional string-array args field to Hook.

src/types/hooks.ts

plugin.tsType plugin hook arguments +2/-0

Type plugin hook arguments

• Adds an optional exec-form args field to UnifiedHookEntry.

src/types/plugin.ts

providers.tsDeclare provider support for exec-form hooks +5/-0

Declare provider support for exec-form hooks

• Adds the optional supportsExecArgs flag to HooksIntegration.

src/types/providers.ts

Bug fix (26) +614 / -150
install.ps1Make the installer compatible with PowerShell 5.1 +15/-15

Make the installer compatible with PowerShell 5.1

• Replaces non-ASCII output and the unsupported escape sequence so the script parses under Windows PowerShell 5.1.

install.ps1

adapter.tsPreserve dot directories in plugin source paths +1/-1

Preserve dot directories in plugin source paths

• Removes only a leading './' or bare '.' rather than stripping a meaningful leading dot.

registries/claude-plugins/adapter.ts

clean-project.tsKeep project-owned local skills during clean +52/-4

Keep project-owned local skills during clean

• Protects declared local sources, including symlink aliases and managed paths. Restricts fallback provider cleanup when no providers were recorded.

src/cli/commands/clean-project.ts

load-env.tsLimit required variables to substituted fields +2/-2

Limit required variables to substituted fields

• Stops treating placeholders in prose and skill content as required credentials.

src/cli/commands/install-tasks/load-env.ts

write-lockfile.tsTreat lockfile write errors as install failures +3/-1

Treat lockfile write errors as install failures

• Records a failed lockfile write as a failure rather than only a warning.

src/cli/commands/install-tasks/write-lockfile.ts

plugin-install.tsFix plugin copies, hook arguments, and skill collisions +168/-26

Fix plugin copies, hook arguments, and skill collisions

• Dereferences safe in-repository links, preserves exec-form hook arguments, and exports 'CLAUDE_PLUGIN_ROOT' when needed. Renames later plugins' colliding skills instead of overwriting them.

src/cli/commands/plugin-install.ts

upgrade.tsRun the Windows installer in the foreground +8/-30

Run the Windows installer in the foreground

• Inherits installer output and checks its result instead of starting it detached with errors hidden.

src/cli/commands/upgrade.ts

plugin-hook-merge.tsInclude arguments in the hook merge key +4/-0

Include arguments in the hook merge key

• Prevents hooks with the same executable but different argument arrays from merging.

src/cli/utils/hooks/plugin-hook-merge.ts

migrate-secrets.tsPreserve secrets that cannot be decrypted +18/-9

Preserve secrets that cannot be decrypted

• Counts undecryptable stored secrets without rewriting them.

src/db/migrate-secrets.ts

capabilities-routes.tsReport only genuinely required variables +2/-2

Report only genuinely required variables

• Uses substituted fields rather than all placeholder-containing content when building variable responses.

src/server/capabilities-routes.ts

configure-routes.tsIgnore prose when checking required variables +2/-2

Ignore prose when checking required variables

• Uses the narrower required-variable extractor in project configuration.

src/server/configure-routes.ts

secret-redaction.tsShow safe MCP environment values +36/-1

Show safe MCP environment values

• Displays placeholders and non-sensitive literals while blanking credential-like names and values.

src/server/secret-redaction.ts

skill-content.tsResolve renamed plugin skill content +12/-1

Resolve renamed plugin skill content

• Uses the saved plugin-relative skill path to find SKILL.md after a name collision.

src/server/skill-content.ts

mirror.tsValidate git ref names before resolution +21/-0

Validate git ref names before resolution

• Rejects unsafe version, ref, and pinned-commit values before they reach git.

src/shared/cache/mirror.ts

snapshot.tsRequire a hexadecimal pinned SHA for snapshot paths +2/-1

Require a hexadecimal pinned SHA for snapshot paths

• Prevents a merely 40-character pin from being used as a snapshot path segment.

src/shared/cache/snapshot.ts

hooks-validate.tsPreserve validated hook arguments +7/-0

Preserve validated hook arguments

• Accepts non-empty string argument arrays on command hooks.

src/shared/hooks-validate.ts

lockfile.tsStamp generator only when pins change +3/-1

Stamp generator only when pins change

• Leaves the previous generator on a no-op install and records the current version when pins change.

src/shared/lockfile.ts

hooks-parser.tsParse plugin hook argument arrays +8/-0

Parse plugin hook argument arrays

• Keeps exec-form args when unpacking command hooks.

src/shared/plugin-manifest/hooks-parser.ts

types-helpers.tsDiscover symlinked plugin skill directories +2/-1

Discover symlinked plugin skill directories

• Includes symlinked skill directories when a reachable SKILL.md exists.

src/shared/plugin-manifest/types-helpers.ts

hook-handlers.tsPreserve hook args and remove stale entries +65/-4

Preserve hook args and remove stale entries

• Passes args through for exec-capable providers or shell-quotes them otherwise. Removes old tagged entries when their event or matcher changes.

src/shared/providers/hook-handlers.ts

parse.tsKeep dot directories in marketplace sources +4/-2

Keep dot directories in marketplace sources

• Corrects relative source and plugin-root normalization.

src/shared/registries/claude-marketplace/parse.ts

paths.tsValidate plugin preview paths +3/-1

Validate plugin preview paths

• Applies the install path guard to marketplace preview subpaths.

src/shared/registries/claude-marketplace/paths.ts

sources.tsKeep dot directories when joining plugin roots +1/-1

Keep dot directories when joining plugin roots

• Strips only a leading './' or bare '.' from relative paths.

src/shared/registries/claude-marketplace/sources.ts

secret-crypto.tsNever re-encrypt undecryptable ciphertext +15/-5

Never re-encrypt undecryptable ciphertext

• Returns an undecryptable flag and leaves ciphertext unchanged when the current key cannot decrypt it.

src/shared/secret-crypto.ts

skill-copy.tsCopy symlink targets safely and once +119/-40

Copy symlink targets safely and once

• Copies real directories before links, keeps traversal inside an allowed root, and avoids repeated directories and cycles.

src/shared/skill-copy.ts

variable-resolver.tsExtract variables only from substituted fields +41/-0

Extract variables only from substituted fields

• Checks MCP server definitions and command tool specs while ignoring prose and plugin-sourced content.

src/shared/variable-resolver.ts

Tests (20) +1105 / -6
clean-project-skills.test.tsTest preservation of local skills during clean +84/-0

Test preservation of local skills during clean

• Covers passthrough installs, OpenClaw, symlinked sources, and previously recorded managed paths.

src/cli/commands/tests/clean-project-skills.test.ts

plugin-resolve-isolate.test.tsUpdate plugin copy stamp fixture +1/-1

Update plugin copy stamp fixture

• Adjusts the existing isolation test to use the new copy-v2 stamp.

src/cli/commands/tests/plugin-resolve-isolate.test.ts

plugin-root-env.test.tsTest plugin-root environment handling +63/-0

Test plugin-root environment handling

• Checks runtime-read detection, command wrapping, exec-form conversion, and shell execution.

src/cli/commands/tests/plugin-root-env.test.ts

plugin-skill-conflicts.test.tsTest colliding plugin skill names +159/-0

Test colliding plugin skill names

• Covers first-owner precedence, renamed skills, viewer resolution, occupied IDs, and SKILL.md names.

src/cli/commands/tests/plugin-skill-conflicts.test.ts

plugin-stable-copy.test.tsTest stable copies of symlinked plugin skills +21/-1

Test stable copies of symlinked plugin skills

• Checks that a GitLab-style linked skill is discovered and copied as real files.

src/cli/commands/tests/plugin-stable-copy.test.ts

status-project.test.tsTest project status and install bookkeeping +200/-0

Test project status and install bookkeeping

• Covers project states, drift, missing variables, read-only access, failure recording, and URL redaction.

src/cli/commands/tests/status-project.test.ts

upgrade-plan.test.tsGuard installer ASCII compatibility +9/-0

Guard installer ASCII compatibility

• Fails if install.ps1 contains non-ASCII bytes that Windows PowerShell 5.1 could misread.

src/cli/commands/tests/upgrade-plan.test.ts

plugin-hook-merge.test.tsTest hook deduplication with argument arrays +21/-0

Test hook deduplication with argument arrays

• Checks that differing arguments and argument boundaries produce distinct merge keys.

src/cli/utils/hooks/tests/plugin-hook-merge.test.ts

secret-at-rest.test.tsTest recovery after a wrong master key +42/-0

Test recovery after a wrong master key

• Verifies ciphertext is unchanged under the wrong key and works again when the original key is restored.

src/db/tests/secret-at-rest.test.ts

mcp-handler.integration.test.tsTest tool discovery guidance +86/-1

Test tool discovery guidance

• Covers unknown and inactive tool errors, exposure-policy messages, and exact-name searches.

src/server/tests/mcp-handler.integration.test.ts

secret-redaction.test.tsTest MCP environment display and redaction +54/-2

Test MCP environment display and redaction

• Verifies placeholders and plain values remain visible while credential-like literals are hidden.

src/server/tests/secret-redaction.test.ts

cache.test.tsTest git ref-name rejection +15/-0

Test git ref-name rejection

• Exercises invalid revision syntax, unsafe pins, and valid ref-name examples.

src/shared/tests/cache.test.ts

capabilities.test.tsTest generated schema and schema references +29/-1

Test generated schema and schema references

• Checks the committed schema stays in sync, optional null fields work, and new files get the schema reference.

src/shared/tests/capabilities.test.ts

lockfile.test.tsTest generator version stamping +15/-0

Test generator version stamping

• Checks that generator changes only when lockfile pins change.

src/shared/tests/lockfile.test.ts

skill-copy.test.tsTest symlink-safe tree copying +69/-0

Test symlink-safe tree copying

• Covers in-repository links, bounded link fan-out, broken links, and repository escapes.

src/shared/tests/skill-copy.test.ts

tool-search.test.tsTest exact tool-name ranking +45/-0

Test exact tool-name ranking

• Checks qualified and remote name matching, suggestions, and tool-name query detection.

src/shared/tests/tool-search.test.ts

variable-resolver.test.tsTest required-variable extraction +27/-0

Test required-variable extraction

• Verifies only substituted fields contribute required variables, not descriptions or skill bodies.

src/shared/tests/variable-resolver.test.ts

full-unpack.test.tsTest plugin exec-form hook parsing +24/-0

Test plugin exec-form hook parsing

• Checks that string arguments survive parsing and non-string arrays are ignored.

src/shared/plugin-manifest/tests/full-unpack.test.ts

hook-handlers.test.tsTest hook replacement and exec-form output +117/-0

Test hook replacement and exec-form output

• Covers moving tagged hooks between events or matchers and preserving or quoting arguments by provider.

src/shared/providers/tests/hook-handlers.test.ts

claude-marketplace.test.tsTest marketplace dot-directory paths +24/-0

Test marketplace dot-directory paths

• Ensures relative plugin sources preserve meaningful leading dots.

src/shared/registries/tests/claude-marketplace.test.ts

Documentation (3) +14 / -2
README.mdDocument JSON Schema association +6/-0

Document JSON Schema association

• Shows how to add the schema reference to an existing YAML capabilities file.

README.md

capabilities-schema.mdDocument schema and plugin data lifetime +3/-1

Document schema and plugin data lifetime

• Describes the published schema and clarifies the '${PLUGIN_DATA}' path and cleanup behaviour.

skills/capabilities-manager/references/capabilities-schema.md

commands.mdDocument project status and flags +5/-1

Document project status and flags

• Explains project states and the 'capa status --json' and '--strict' options.

skills/capabilities-manager/references/commands.md

Other (3) +17 / -3
security.ymlBump CodeQL action to 4.38.3 +2/-2

Bump CodeQL action to 4.38.3

• Updates the CodeQL init and analyze actions from 4.38.2 to 4.38.3.

.github/workflows/security.yml

generate-schema.tsGenerate the capabilities JSON Schema +14/-0

Generate the capabilities JSON Schema

• Writes the published schema from the application's zod schema.

scripts/generate-schema.ts

bundled.tsUpdate bundled marketplace adapter pin +1/-1

Update bundled marketplace adapter pin

• Changes the pinned hash to match the adapter fix.

src/shared/registries/bundled.ts

Qodo review of #351:

- Plugin agents keep skills that were installed under a prefix after a
  name clash (they referenced the old id and dropped the skill).
- A plugin that fails part-way releases the skill names it claimed, so
  a later plugin keeps the plain name.
- A lockfile pin must be a hex commit SHA (7 to 40 characters); a
  branch or tag there would float.
- A legacy plaintext secret that merely starts with enc:v1: is
  encrypted on open; only a well-formed envelope is treated as
  ciphertext and left untouched when it doesn't decrypt.
- Search explains a missing tool name with the server's expose policy,
  sharing exposurePolicyReason with the unknown-tool error.
- MCP env redaction also blanks token-only URL userinfo, credential
  parameters in query and connection strings, and base64-style secrets
  with / or +.
Minitour and others added 8 commits October 23, 2026 14:54
Qodo review of #352:

- An encrypted empty secret is only IV + tag (28 bytes); requiring 29
  made the migration encrypt it again and reads return ciphertext.
- Redaction: URL userinfo is a credential only as user:pass@ or a long
  mixed token (so ssh://git@github.com stays visible), and a base64-style
  value needs + or = or the 40 characters of an AWS secret key (so
  relative paths stay visible).
- A failed plugin restores the skill-claim snapshot taken before it
  instead of deleting claims by install id, which another entry may
  share.
- Sub-agent search judges "callable elsewhere" by the tools the caller
  can see.
- buildSearchPayload takes hidden and note separately instead of
  checking a message prefix.
- Doc comments sit above their own functions again.
Token-only URL userinfo counts at 16+ characters, letters only included.
Base64-style values are told apart from relative paths by Shannon
entropy instead of requiring + / = or exactly 40 characters.
The entropy check alone let low-entropy AWS-style keys through. The
fixtures named *_TOKEN / *_SECRET were blanked by name before the value
rules ran, so the tests did not cover them.
Redaction: keep the 40-character key rule, test value rules directly
Redaction: letter-only URL tokens and unpadded base64 secrets
fix: second round of v2.4.0 release review follow-ups
fix: follow-ups from the v2.4.0 release review
The running server's capabilities carry the tools synthesized from each
server's expose policy, so serverIdsWithDeclaredTools treated every policy
server as hand-curated and exposurePolicyReason never explained a tool
hidden by expose: except.
explainGitError rewrote every error. Invalid refs and lockfile pins
came out as 'Failed to clone ...: Unknown error', and a missing tag on
GitHub/GitLab as 'repository not accessible'. Only a failed git process
(cmd / code set) is explained now; capa's own messages pass through.
Minitour and others added 2 commits October 23, 2026 18:00
Search mode: explain tools hidden by expose: except on a running server
With other plugins resolving, the failure was only a warning, so capa
status showed Installed and --strict exited 0. It is now recorded like a
failed skill, and reported once instead of twice when every plugin fails.
Minitour and others added 7 commits October 23, 2026 18:07
explainGitError told git failures from capa's own errors by the cmd /
code properties execFile sets. That shape depends on the runtime, so
git() now records the errors it rejects with and the wrapper checks that.
The header added servers.length to the authored tools, so 8 servers with
100 tools showed 8. It now counts every exposed tool, as the projects
list does.
UI: show the number of tools, not servers, in the Tools header
Install: show invalid ref and pin errors instead of Unknown error
resolvePlugins now returns failures alongside warnings. The install
task and passthrough install use that list, so a warning from a plugin
that resolved can't count as a failure because of its text.
Plugin resolve failures: structured list instead of text match
…ures

Status: count plugins that fail to resolve as install failures
@Minitour
Minitour merged commit d7af119 into main Oct 23, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment