Repository navigation
fix(claude-config): report-only plugin drift posture with a dependency carve-out and CR-safe keys - #4515
Conversation
check-plugin-drift.sh compares keys in jq, prints the not-diffed coverage line, exits 1 only for orphans. fix-plugin-drift.sh drops auto-add, holds a removal a lower-precedence true would expose, and backs up via mktemp. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NEW is report only, a false key is inventory unless an enabled plugin depends on it, and the backup name and coverage line are documented. check-doc-citations.sh accepts nested page slugs for the two plugin pages now cited. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ependencies The manifest is optional, so a resolved install path without .claude-plugin/plugin.json yields no row. Only an unresolved path, an unreadable or invalid plugin.json, or a non-array dependencies is not-inspectable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ncy basis record Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…engine - check-plugin-drift.sh: the shared jq defs take the marketplace index as a parameter, so no call depends on an unbound $i (jq 1.6 refuses one). - Orphans carry their exact value. Only a key holding exactly false is auto-removed; true and any other value (null, string, number, object) go to manual review in fix-plugin-drift.sh, and the engine never calls them removable. - audit-engine.sh: drift-skipped, orphan and rename rows go through the same JSON and base64 path as the drift-new and dependency rows. An empty field travels as a `-` sentinel, a row with the wrong field count or an undecodable field becomes a not-inspectable row, and the decoder tries base64 -d, --decode and -D and reports failure. - check-plugin-drift.sh writes [] to SETTINGS_AUDIT_OUTPUT_JSON when no marketplace is declared, skips a source.repo that is not owner/name, and fetches with curl --globoff. - fix-plugin-drift.sh fails the lower-scope guard closed behind an unsearchable directory, clears TMP_EOL after the rename, and prints control characters in the settings and backup paths as `?`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the code and the cited docs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Claude finished @kyle-sexton's task in 4m 40s —— View job Security review
Reviewed the full diff ( Most of this PR is itself security-hardening (JSON-safe key handling to close the CRLF-key data loss bug, 1 finding, IMPORTANT:
No other findings met the high-signal bar (exploitable path in this diff, not pre-existing/style/generic advice). |
|
Claude finished @kyle-sexton's task in 4m 24s —— View job Claude code review in progress
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5d050a699c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…S check Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 2m 27s —— View job Security review
Re-ran the full security review for this push. The only commit since the last review ( 1. IMPORTANT — path traversal via unsanitized 2. IMPORTANT — backup write reopens the No other findings met the high-signal bar (exploitable path in this diff, not pre-existing/style/generic advice). The rest of the diff's security-relevant hardening — CR-safe key handling via JSON end-to-end, the |
…alidate citation slugs fix-plugin-drift.sh created the backup with mktemp and then reopened the name with a plain redirect, so a writer in the settings directory could swap the file for a link in between and the snapshot was written through it. The name now comes from mktemp -u, is refused when anything exists there, and is written on one noclobber open under umask 077. Before any byte is written the descriptor must be a regular file that is the one at the name, which also refuses a symlink to a non-regular target that noclobber would open. A failure before this run created the file leaves what is at the name alone. check-doc-citations.sh interpolated manifest slugs into paths, so a `../` or absolute slug escaped the fetch, docs and fixture directories. Every slug is now checked against ^[a-z0-9_-]+(/[a-z0-9_-]+)*$ before any page is read, exiting 2 with the row named. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 2m 18s —— View job Security review
Re-reviewed after the push since the last security review ( 1. Path traversal via unsanitized 2. Backup-write symlink race in No new security-relevant findings in this push — the diff outside these two fixes is limited to changelog/docs text describing the same change and matching test coverage. Status: both previously-reported findings are resolved. No open findings. |
|
Claude finished @kyle-sexton's task in 2m 37s —— View job Claude code review in progress
|
…-plugin record An undocumented top-level key recorded in skills/audit/reference/consent-receipts.json is an A/consent-receipt ok row (claim consent-receipt:<key>) instead of an undocumented-key finding when the file's scope is one the record declares, a plugin@marketplace owner is true in the merged enabledPlugins, and for the claude-code owner a searched binary still carries the name. Otherwise the finding stays and its detail names the failed gate; a missing or invalid record file is one not-inspectable row. The key match is checked in jq against the scope JSON, so a carriage-return twin of a recorded key is never labeled. First record: skipWorkflowUsageWarning, user scope. SETTINGS_AUDIT_ENGINE_CONSENT_RECEIPTS_FILE overrides the record for tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 1m 48s —— View job Security review
Re-reviewed after the push since the last security review (
New commit ( The new Checked whether the new label could be used to mask a real undocumented-key finding an attacker controls: the match requires the key to literally equal an entry in the repo-shipped The one asymmetry I looked at closely — the Status: no new findings. Both previously-reported findings remain resolved. |
|
Claude finished @kyle-sexton's task in 4m 12s —— View job Claude code review
Scope of this passThis lane already ran several full rounds on this PR (Codex + Claude code/security reviews through commits The only commit since the last full review is Consent-receipt labeling (
|
…eipts check_keys strips carriage returns, so a settings file holding both a recorded key and its carriage-return variant produced two ok consent-receipt rows. A scope now labels the key only when exactly one of its keys reads as the record key with carriage returns removed; otherwise both stay undocumented-key findings whose detail names the variant. A record whose string fields, or owner id, hold a control character (U+0000-U+001F) makes the record file invalid, so a NUL in a field can no longer shift the NUL-separated fields; the file takes the existing not-inspectable fallback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 7m 22s —— View job Claude code review
Scope of this passThis lane already completed full code-review passes through commit
|
|
Claude finished @kyle-sexton's task in 1m 15s —— View job Security review
Re-reviewed after the push since the last security review (
New commit (
|
A consent-receipt record file that lists one key under two owners is now invalid: the audit reports one not-inspectable row and the key keeps its undocumented-key finding. A stale claude-code verdict (the searched binary lacks the name) is terminal for the key, so no other owner can label it. Case 46 covers the duplicate-owner file; case 47 exercises the stale guard through cr_match directly, since a valid file cannot reach it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed settings paths The drift and engine suites clear every inherited SETTINGS_AUDIT_* variable and CLAUDE_PROJECT_DIR, and pin HOME, CLAUDE_CONFIG_DIR and CLAUDE_SETTINGS_FILE to fixtures under the suite temp dir. In the fix suite every --yes run goes through guard_apply, which aborts the suite unless the target lies under the suite temp dir; a new case proves it refuses outside paths without running an apply. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 2m 40s —— View job Claude code review
Scope of this passThis lane already completed a full code-review pass through commit
I reviewed both in full against
No correctness issues found in either commit. Nothing here needs an inline comment — I found no candidate findings to bring to adversarial verification. Caveat on verification: this session's Bash tool doesn't have permission to execute the repository's own shell scripts/test suites (only |
|
Claude finished @kyle-sexton's task in 1m 23s —— View job Security review
Two commits landed since the last full security-review pass on this PR (
|
The newest ci run on this PR came from a body edit. That event skips every lane, and ci-status counts an all-skipped run as a failure, which blocked the merge. This empty commit starts a normal run on the unchanged tree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 46s —— View job Claude code review
Skip gate: no new review surface
Per For reference, the two issues raised earlier in this PR's review history (path traversal via unsanitized |
|
Claude finished @kyle-sexton's task in 36s —— View job Security review
Skip gate: no new security-relevant surface
Per this lane's skip gate: "This head already has a successful security review that still applies." The last full security-review pass on this PR ran through commit
Since the tree at Status: no findings. Skipped — no code change since the last completed pass. |

No related issue: closes handoff items 20260924-110000 and 20260925-080000; the large remainder (D1-D8 and two engine gate defects) is tracked in #4514.
Summary
The claude-config audit stops asking operators to record an explicit
trueorfalsefor everycatalog plugin, and stops writing
falsekeys itself. The two drift scripts also carry plugin andmarketplace keys as JSON from end to end, so a key that ends in a carriage return is no longer lost.
The operator approved the posture on 2026-09-25: Q3 and Q5 as vetted, and the Q4 dependency
carve-out reading
falsekeys from all three scopes. Release 0.49.0.Fix
audit-engine.shno longer emits thedrift-newfinding. Instead it emits oneokinventory row per marketplace, counting catalog plugins that have no
enabledPluginsentry inany scope.
disabled-pluginfinding is now anokinventory row for everyfalsekey in theuser, project and local files, and the row says when a higher-scope
trueshadows the key. Onecase stays a finding: a
falsethat wins the local > project > user merge and that an enabledplugin declares as a direct dependency. That is a
warningfinding,dependency-disabled:<key>.Dependencies are read from each enabled plugin's
.claude-plugin/plugin.json. The manifest isoptional. An unresolved install path, an invalid manifest, or a non-array
dependenciesproducesa
not-inspectablerow.fix-plugin-drift.sh --yesnever adds a key, and NEW upstream plugins are listedfor reporting only. An orphan removal proceeds only when the key's live value is exactly
false.It moves to manual review when a lower-precedence scope file holds
true, or when that filecannot be read. The plan says that other developers' scopes and managed settings were not
checked.
Not diffed:line listing the keyswhose marketplace the audited file does not declare. The engine reports the same gap as an
E/driftskiprow.tr -d '\r'orjq -Rtouches akey. The engine compares keys in jq, and it runs jq with MSYS argument conversion disabled.
mktempname,<settings>.bak.<stamp>.<random>. No existingpath is written through, which closes the non-regular-file window.
point HOME and CLAUDE_CONFIG_DIR at fixtures.
skills/audit/reference/consent-receipts.json, shaped{"consentReceipt": {"<owner>": [records]}}.plugin@marketplaceid or the reserved idclaude-code.The first entry is
claude-code->skipWorkflowUsageWarning, scoped to the user file.okrow,A/consent-receipt, in place of theundocumented-keyfinding, but only when all of these hold:claude-code, the binary search did not find the namemissing.
"stale consent receipt".
not-inspectablerow. A key that is ambiguousbecause a carriage-return twin is present is not labeled. A record whose fields contain a
control character makes the file invalid.
.claude/audit-pass.mdsuppression ofundocumented-key:<key>stops matching oncethat key is labeled. CHANGELOG says so.
claims carry a four-part record.
Verification
These results were measured on Windows 11 with Git Bash and native jq 1.8.2.
mk\rwas reported asmk (no-repo)SKIP.TMPDIR='C:/...' bash fix-plugin-drift.test.shfailed 6 of 182 checks, in cases 29 and 30.check-plugin-drift.test.sh: 96/96, and the same with aC:/TMPDIR.fix-plugin-drift.test.sh: 271 pass, 0 fail, 1 skip, both with and without aC:/TMPDIR. The skip is case 40: chmod cannot clear a directory's search bit on this host.audit-engine.test.sh: 270/270.check-doc-citations.test.sh: 23/23.check-changelog-parity.sh --check-bump 47555b587passes.check-changed-skills.shfails the audit skill only oncheck-hook-coverage.test.sh(6/93). That suite fails the same 6 checks on untouched base on this host, and this PR does not touch it.--yesadds no key.check-doc-citations.test.sh: 35/35. Its new case 9 failed 12 checks before the fix.fix-plugin-drift.test.sh: 277 pass, 0 fail, 4 skip, both with and without aC:/TMPDIR. The skips are symlink and mode-bit variants that this host cannot run.also red first.
audit-engine.test.sh: 294/294 on f0c801b.claude-codeverdict is terminal. The new checks in cases 46 and 47 failed 5 of 31 on f0c801b.
before its first case. Every
--yesrun in the fix suite goes throughguard_apply, whichaborts the suite for any path outside its temp dir.
CLAUDE_SETTINGS_FILEwas byte-identical after both driftsuites ran.
audit-engine.test.sh: 301/301.fix-plugin-drift.test.sh: 287 pass, 0 fail, 4 skip, both with and without aC:/TMPDIR.check-plugin-drift.test.sh: 96/96.\bin a test, the one CI lint failure on 5d050a6.check-changed-skills.sh), fresh-context verifier.Related
(fix-plugin-drift remainder after fix(claude-config): fix-plugin-drift reports skipped runs and refuses stale plans and concurrent writes #4490). claude-config audit: derive the remaining criteria from docs (D1-D8) and two engine gate defects #4514 carries D1-D8,
sr_sectionrescans, and theU+0000 key split. It also carries stale "skips directory sources" text in
docs/cloud-sessions.mdand
scripts/check-plugin-catalog-enablement.sh, which sit outside this plugin.no default was challenged by one fresh-context validator with the rationale withheld, and its
verdict was adopted:
narrow scope, and the coverage line replaces a machine-wide census. The coverage row names the
local file's keys too. The row never double-reports keys whose marketplace is unregistered.
plugin@marketplace. Theoperator wrote "consent-receipt key named consentReceipt, keyed by plugin id".
chose reading 2.
in full:
consentReceiptbecomes the JSON key.claude-codereceipt must not bypass the binary tie-breaker, so an absent name isreported as stale.
the shared project file.
.claude/audit-pass.mdalready acceptsundocumented keys.
claude-code, with no@, so it can never collide with a real pluginid.
key, and control characters in record fields) were fixed. Its third item, disclosure of the
pre-existing
SETTINGS_AUDIT_ENGINE_*test overrides, was declined as out of scope: thatclass predates this PR, and the new override adds no reach.
trueblocks the removal, and the plan names the scopes it could not check.--docs-dircovers docs only when both pages aresupplied, and the drift catalogs still fetch unless
SETTINGS_AUDIT_ENGINE_SKIP_DRIFT=1.install. The absent-entry reading keeps the vet's range (observed disabled on 2.1.278 to
2.1.282), and this PR did not re-measure it.
🤖 Generated with Claude Code