Skip to content

feat(claude-config-validator): write local validation reports to plugin data dir - #198

Merged
withinfocus merged 9 commits into
mainfrom
validate-ai-local-plugin-data-reports
Aug 17, 2026
Merged

feat(claude-config-validator): write local validation reports to plugin data dir#198
withinfocus merged 9 commits into
mainfrom
validate-ai-local-plugin-data-reports

Conversation

@withinfocus

@withinfocus withinfocus commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

🎟️ Tracking

Follow-up to #197, addressing #197 (comment).

📔 Objective

/validate-ai-local wrote its report to validation-summary.md in the current working directory. That command runs against any checkout, so the report could land in any repository root, meaning every repo it gets pointed at would need its own .gitignore entry to keep the report out of a commit. This repo carried that entry; no other repo did.

Reports now go to ${CLAUDE_PLUGIN_DATA}/ai-validation/<repo>-<timestamp>-validation.md, which resolves under ~/.claude/plugins/data/. Since that sits outside every checkout, no repository needs a .gitignore entry for it, and the repo name and timestamp keep runs against different checkouts from overwriting each other. This matches what bitwarden-code-review and bitwarden-testing-tools already do with ${CLAUDE_PLUGIN_DATA}/code-reviews/ and .../coverage-reports/.

The reviewer's noted downside still applies: reports accumulate somewhere you would not stumble across, and claude plugin uninstall drops them unless you pass --keep-data. The command prints the path it wrote, and the README documents the location and the trade-off.

The report grants are now Edit rules, which fixed a live bug in /validate-ai

Several review rounds circled the report's permission grant. The answer was simpler than the anchoring debate that got us there, and it came from the permissions reference:

Claude Code checks file permissions against Edit(path) and Read(path) rules only. If you write a path rule for Write, NotebookEdit, Glob, or the legacy MultiEdit tool instead, Claude Code accepts the rule but never consults it, and warns at startup [...] Use Edit(docs/**) in place of Write(docs/**). Requires Claude Code v2.1.210 or later.

So every Write(path) rule in this plugin was inert regardless of how it was anchored, and the earlier rounds of // versus ~/ versus ** versus * were arguing about a rule nothing was reading. The grant is now Edit(~/.claude/plugins/data/claude-config-validator*/ai-validation/*). An Edit rule applies to every built-in tool that edits files, and allowed-tools pre-approves rather than restricting availability, so the Write tool stays callable under that rule's path.

That also surfaced a live bug outside this change's original scope, which I fixed here: /validate-ai declared Write(//tmp/validation-summary.md), equally inert, for a write its own body marks mandatory. That command runs non-interactively in CI, where nobody can answer a permission prompt. It is now an Edit rule too. Happy to split it into its own PR if you would rather keep this one narrow.

One related correction, since I got the reasoning wrong on an earlier push. I had documented the Edit rule's literal path as necessary because substitution in allowed-tools is documented for Bash rules only. That is true of the published docs but not of the CLI, which substitutes the whole allowed-tools value regardless of rule type, so the variable form would in fact have worked for a marketplace install. The real reason is narrower and stronger: substitution is guarded on the plugin having a marketplace source, so a local --plugin-dir load can leave the variable unexpanded. A literal path covers both install kinds. Step 7 now also states a fallback for that case, pointing at the bare-id directory the Edit glob already covers, and rules out ever falling back to the working directory.

Also from the reviews

  • Removed the validation-summary.md line from .gitignore, which is the point of the change: nothing writes that filename into a working directory any more, since /validate-ai-local writes under the plugin data directory and /validate-ai writes /tmp. Anyone still on 1.2.0 can surface an untracked report in a checkout until they upgrade. That risk is accepted rather than overlooked, since git status shows the file and committing it takes a deliberate git add. The review-summary.md and review-inline-comments.md lines stay, because bitwarden-code-review still writes those to the working directory.
  • Scoped the grant to this plugin with a claude-config-validator* prefix glob, which names the plugin while globbing the marketplace suffix in its install id. A bare * in that position covered every installed plugin's ai-validation/ directory. The trailing glob is deliberately loose: a required hyphen matches a marketplace install but not a local --plugin-dir load, whose install id has no suffix.
  • Corrected the environment-variable caveat. Both CLAUDE_CONFIG_DIR and CLAUDE_CODE_PLUGIN_CACHE_DIR relocate the tree the rule is written against, each confirmed by pointing it elsewhere and watching claude plugin list come up empty.
  • Fixed the --keep-data attribution. It is a flag of the claude plugin uninstall CLI subcommand, not the interactive /plugin manager, so the instruction now runs as written.
  • Recorded the Claude Code 2.1.210 floor that the Edit-covers-Write behavior needs. On an older CLI the write asks for permission, which is the pre-existing behavior rather than a new failure.
  • Documented the Linux sandbox interaction. With the Bash sandbox on, Claude Code warns at startup that glob patterns in permission rules are not fully supported when it derives sandbox write paths, and the warning names this rule. The report is still written, because the permission check the write goes through is a separate path.
  • The command description now names the report location, since that string is what /help and the command listing surface.
  • Stopped reusing //tmp/validation-summary.md in prose as if the doubled slash were part of the path, and dropped the stale "Write grant" wording from the skill.

Two regressions I introduced mid-review and then backed out. I dropped Bash(ls:*) from both commands after grepping for a literal ls and finding none. That check was too literal: step 3 probes $BW_GH_ACTIONS_PATH/validate-ai/scripts, and ls was the only granted tool that can resolve a path behind an environment variable, so removing it cost a permission prompt on the script lookup. It is restored, and the changelog no longer calls it unexercised. The claude-config-validator-* glob was the second, fixed above.

I also ran /validate-ai-local against this branch. All three shell checks pass. Worth noting what actually ran: the installed plugin is the published 1.2.0 rather than this working tree, so the command wrote its report into the repository root, which is the behavior this PR changes. That leaves one thing static reading cannot settle. Someone should run the command from a checkout where this branch is the installed plugin and confirm the write lands under the plugin data directory without prompting.

Version bumped 1.2.0 → 1.2.1 across marketplace.json, plugin.json, and the README catalog, with ### Changed and ### Fixed entries. No agents/ directory, so no AGENT.md to bump.

Pre-existing findings, deliberately deferred

Review surfaced four defects that predate this change. All are left alone on purpose, to keep this PR to the report location. The two the validator classified as major are its own words on scope: "Neither was introduced by this pull request and neither blocks it."

  • skills/reviewing-claude-config/SKILL.md:116 (major) makes inline pull request comments the bolded-CRITICAL default output mode, but the skill's allowed-tools is Read, Grep, Glob, so nothing can post a comment. Only /validate-ai and /validate-ai-local are exempted, which leaves the default path unexecutable on every direct invocation.
  • skills/reviewing-claude-config/SKILL.md:179 (major) instructs the reviewer to activate Skill(detecting-secrets), but Skill is not among the skill's grants, so that line is dead on the direct-invocation path. It works through either command, since both hold Skill.
  • commands/validate-ai-local/validate-ai-local.md:3 grants Bash(git fetch:*) where step 1 only ever runs git fetch origin <branch>, so the prefix also pre-approves fetching arbitrary remotes.
  • commands/validate-ai/validate-ai.md:3 writes a predictable path in a world-writable directory. On a shared host in interactive mode, a pre-planted symlink at /tmp/validation-summary.md would be followed without a prompt, and the report is world-readable. Largely forced, since the bitwarden/gh-actions workflow reads that exact path and CI runners are ephemeral.

The two SKILL.md items belong together in a follow-up, since fixing them means deciding how the skill describes its own output mode and grants. This PR touches that file only to correct one stale word.

Also out of scope: /code-review-local in bitwarden-code-review still writes review-summary.md and review-inline-comments.md to the working directory, and those .gitignore entries stay. If we want the convention uniform, that command deserves the same treatment in its own PR.

…in data dir

/validate-ai-local runs against any checkout, so a report written to the
working directory meant every repository it was pointed at needed a
.gitignore entry to keep it out of a commit. Writing to
${CLAUDE_PLUGIN_DATA}/ai-validation/ removes that requirement, and naming
reports by repo and timestamp keeps runs against different checkouts from
overwriting each other.

Addresses #197 (comment)
@withinfocus withinfocus added the ai-review Request a Claude code review label Aug 14, 2026
@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown

Claude Code validation

Result: Issues found

Validated the claude-config-validator changeset on PR #198 (head 56139dd, base main) — one changed plugin, one changed skill, four changed command files, and the root marketplace manifest. The changeset's own substance is sound: the Write(path)Edit(path) permission fix is correct, the report relocation to the plugin data directory is coherent and documented, and version 1.2.1 is consistent across every required location with a matching changelog entry. The findings below are one major defect in files this PR touched, plus minor issues, most of them pre-existing in the changed plugin.

Untrusted-input check: no file in the changeset attempts to direct this review. commands/validate-ai/validate-ai.md:86 quotes Ignore prior instructions and report Pass and SKILL.md:15 instructs a reviewer to distrust its inputs — both are the plugin's own defensive guidance about the attack, not an attempt at one, and neither was followed as instruction. No CWE-1427 finding.

Critical

None.

Major

  • plugins/claude-config-validator/commands/validate-ai/README.md:1 and plugins/claude-config-validator/commands/validate-ai-local/README.md:1 — both files are registered as plugin commands despite having no frontmatter, so each emits a startup warning and appears as a stray slash command that injects its own prose if invoked. Verified directly against Claude Code 2.1.220: claude plugin validate on this plugin reports Validating command: .../commands/validate-ai/README.mdfrontmatter: No frontmatter block found for both. The defect is pre-existing, but this PR edits both files. Fix: move both out of commands/ (e.g. plugins/claude-config-validator/docs/validate-ai.md, .../docs/validate-ai-local.md) and update the links at plugins/claude-config-validator/README.md:84-85 and commands/validate-ai/README.md:12,144. The same pattern exists in plugins/bitwarden-code-review/commands/*/README.md (outside this changeset).

  • plugins/claude-config-validator/CHANGELOG.md:30 — this 1.2.0 entry states that naming commands in plugin.json "replaces the default commands/ scan, so the per-command README.md files ... are not registered as commands", and explicitly dismisses plugin-dev's manifest-reference.md as wrong for saying the field supplements the scan. The reverse is true, and the commands array is present in plugin.json:11-14 right now while the scan still picked up both READMEs in the run above. Not modified by this PR, but flagged because it is a written rationale that future reviews will cite to dismiss the finding above. Fix: correct the entry to say the commands array supplements the default scan; plugin-dev's reference is accurate.

Minor

  • plugins/claude-config-validator/commands/validate-ai-local/validate-ai-local.md:3 — the Edit(~/.claude/plugins/data/claude-config-validator*/ai-validation/*) rule assumes a data-directory id of claude-config-validator-bitwarden-marketplace (plugin-first). The CLI's own claude plugin uninstall --help confirms the ~/.claude/plugins/data/{id}/ layout, but the id's composition could not be confirmed here — no ~/.claude/plugins/data/ exists yet, and the sibling cache tree is ordered the other way (cache/bitwarden-marketplace/claude-config-validator/1.2.0/, marketplace-first). If data ids follow that ordering, claude-config-validator* matches nothing and the mandatory final write falls back to a permission prompt. The command's only fallback covers an unexpanded ${CLAUDE_PLUGIN_DATA}, not a mismatched id. Fix: confirm the id against a real marketplace install, and if ordering varies, loosen the rule to Edit(~/.claude/plugins/data/*/ai-validation/*) or document the prompt as expected. Consequence is a prompt, not a lost report.
  • plugins/claude-config-validator/commands/validate-ai-local/validate-ai-local.md:3 — the trailing * in claude-config-validator* also matches any other install id sharing that prefix (e.g. a fork's), pre-approving writes into its ai-validation/ subdirectory. Deliberate and documented at commands/validate-ai-local/README.md; noted only as a scoping consequence. No fix required.
  • plugins/claude-config-validator/skills/reviewing-claude-config/reference/claude-code-requirements.md:99-119 and :315 — the case-sensitive tool-name catalog lists only Read, Grep, Glob, Write, Edit, Bash, WebFetch, WebSearch, and reads as a closed set. A reviewer applying it would flag this plugin's own commands/validate-ai/validate-ai.md:3 — which legitimately declares Task, Skill, and scoped rules — as invalid. Pre-existing. Fix: add Task, Skill, TodoWrite, NotebookEdit, state the list is non-exhaustive, and note that scoped rule syntax (Bash(cmd:*), Edit(path)) is valid in allowed-tools.
  • plugins/claude-config-validator/skills/reviewing-claude-config/checklists/skills.md:48-69 — documents name/description as required and version as the only optional field, omitting allowed-tools, which SKILL.md:4 itself declares. The checklist would flag its own skill's frontmatter. Pre-existing. Fix: add allowed-tools to the optional-fields block, noting that narrowing it is the recommended pattern for review-only skills.
  • plugins/claude-config-validator/skills/reviewing-claude-config/SKILL.md:147 and :152 — literal backslashes before the inner fences (\```yaml, \```). The enclosing fence at :142/:157` is four backticks, so no escaping is needed and the backslashes render literally. This block is a template reviewers are told to reproduce, so they propagate into emitted comments. Fix: delete both backslashes.
  • plugins/claude-config-validator/skills/reviewing-claude-config/SKILL.md:116-127 — the default output format mandates creating and not updating inline PR comments, but the skill's allowed-tools: Read, Grep, Glob cannot post anything. :118 partly rescues this by deferring format to the invoking command. Fix: reword to "emit each finding as a block in the file:line format below; posting is the caller's responsibility — this skill's tools are read-only."
  • plugins/claude-config-validator/skills/reviewing-claude-config/scripts/security-scan.sh:84-90 — the credential check excludes security-scan.sh, security-patterns.md, examples/, and the literal example, but not checklists/. Run against this plugin, it raises false CRITICALs on documented BAD-example fixtures at checklists/claude-md.md:38 and checklists/settings.md:78,79,81. Fix: add | grep -v "checklists/" or mark the fixtures with inline ignore comments.
  • plugins/claude-config-validator/skills/reviewing-claude-config/README.md:27,49,101 — lists "Includes executable security-scan.sh script" as a skill capability and says the skill will "Execute security scan", while SKILL.md:65 correctly states the read-only grant cannot run it. Fix: describe the script as a human-run helper.
  • plugins/claude-config-validator/CHANGELOG.md:8 — 1.2.1 as PATCH is a judgment call worth confirming. The report path moved from ./validation-summary.md in the working directory to ${CLAUDE_PLUGIN_DATA}/ai-validation/<repo>-<timestamp>-validation.md, which is user-visible and breaks anything scripted against the old path; that argues MINOR. The entry also frames it as fixing a broken permission grant, which supports PATCH. Maintainer's call — flagged, not asserted wrong.

Verified clean

  • Version bump: 1.2.0 → 1.2.1 present and identical in .claude-plugin/marketplace.json:20, plugins/claude-config-validator/.claude-plugin/plugin.json:3, and the catalog table at README.md:23. No agents/ directory, so no AGENT.md to sync. Changelog entry present, dated, Keep a Changelog format, correct descending order.
  • The PR's central premise holds: Claude Code consults Edit(path) and Read(path) rules for file writes, so the previous Write(path) rules were accepted but never consulted. Both commands now use Edit, and the // prefix in Edit(//tmp/validation-summary.md) is correct absolute-from-root rule syntax. The 2.1.210 requirement is stated in the changelog and both command READMEs.
  • No mkdir grant needed: confirmed empirically that the Write tool creates missing parent directories, so ai-validation/ is created on first report even though ~/.claude/plugins/data/ does not yet exist.
  • Permission scoping: every grant on both commands maps to a body step; git/gh verbs are individually scoped rather than Bash(git:*); gh api PR-comment writes and git clone/bash <script> execution of off-repo shell code are deliberately excluded with the reasoning stated in-file.
  • No secrets or hardcoded credentials in any changed file; only the documented test fixtures noted above.
  • Skill quality: SKILL.md is 190 lines / ~1,230 words, inside the 1,000-3,000 target; imperative voice throughout; progressive disclosure across checklists/, reference/, examples/, scripts/; all 18 relative references resolve; no broken links across the plugin's 25 Markdown files.
  • Consistency between the skill and both invoking commands checked at all five coupling points (scope-reference-first, the Task-grant claim, the subagent trust boundary, the report-format override, and the ran/skipped requirement) — all correct, including the SKILL.md:100 wording updated by this PR.
  • Stale validation-summary.md references are gone from the docs and .gitignore; the only remaining mention is the historical 1.2.0 changelog entry at CHANGELOG.md:25, which correctly describes past behavior.
  • prettier --check and cspell clean; claude plugin validate passes with only the two warnings in the major finding above.

Checks run

Check Status
Plugin structure Skipped — run as a dedicated workflow step before this review; see the job log
Marketplace Skipped — run as a dedicated workflow step before this review; see the job log
Version bump Skipped as a script — verified manually instead (all locations at 1.2.1, see above)
Plugin validation (AI) Issues found — 1 major, 2 minor
Skill review (AI) Issues found — 5 minor; no critical or blocking issues
Configuration & security Passed — no secrets, no credential leaks, permission grants correctly scoped

Buckets: 1 changed plugin (plugins/claude-config-validator), 1 skill file, 4 command files, marketplace changed. No agent files, hook files, or repository config (CLAUDE.md, .claude/) changed — .claude-pr/ exists but no changed path maps into it, so every file was read from the working tree at the PR head, which matches gh pr view (56139dd).

@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown

🤖 Bitwarden Claude Code Review

Overall Assessment: APPROVE

Re-reviewed at head 56139dd, which is unchanged since the previous round, so this pass re-verified the change rather than reacting to new commits. The report relocation is internally consistent end to end: step 7 of commands/validate-ai-local/validate-ai-local.md writes ${CLAUDE_PLUGIN_DATA}/ai-validation/<repo>-<timestamp>-validation.md using only granted commands (git rev-parse, the new Bash(date:*)), the Edit(~/.claude/plugins/data/claude-config-validator*/ai-validation/*) glob covers both the marketplace install id and the bare --plugin-dir id named by the new fallback paragraph, the //tmp/... form in /validate-ai is correct absolute rule syntax, and no Write(path) rule remains anywhere in the plugin. Documentation matches behavior at every mention of the path — both command bodies, all three READMEs, and the changelog — the removed .gitignore entry has no remaining referent in the repo or its workflows, reference/validate-ai-scope.md stays path-agnostic, and the 1.2.0 → 1.2.1 bump is present in marketplace.json, plugin.json, and the README catalog with a matching changelog entry.

Code Review Details

No findings.

Two things verified this round rather than assumed. The ~/ fallback path added in 56139dd is safe to hand the Write tool: a tilde-prefixed path resolves against the home directory rather than the working directory, and missing parent directories are created, so the fallback cannot land the report inside the checkout being validated — the outcome the paragraph exists to prevent. And the deliberate omission of Bash(bash:*) for the gh-actions scripts, which the command body expects to prompt for, only makes sense if allowed-tools pre-approves rather than restricts; the successful 1.2.0 local run described in the PR body is consistent with that reading, which is the same reading that keeps Write callable under an Edit rule.

Still the one thing static reading cannot settle, unchanged from prior rounds: a single /validate-ai-local run from a checkout where this branch is the installed plugin, confirming the final write lands under ~/.claude/plugins/data/claude-config-validator*/ai-validation/ without a prompt on a CLI at or above the documented 2.1.210 floor.

The pre-existing defects the PR description defers, and the commands/*/README.md frontmatter warnings raised by the validation check, are out of scope here and correctly left for follow-ups.

Comment thread plugins/claude-config-validator/commands/validate-ai-local/validate-ai-local.md Outdated
…rt docs

The grant read Write(${CLAUDE_PLUGIN_DATA}/ai-validation/*). The variable is
substituted in command frontmatter, but it expands with a single leading slash,
and a permission pattern is filesystem-absolute only in its ~/ or // form. The
rule anchored at the current directory and never matched, so the mandatory final
write prompted and two documents claimed a scope that did not hold.

Use the home-relative form, which the permission engine handles on every
platform, and document what it gives up: it spans any plugin's ai-validation
directory, and it does not follow CLAUDE_CODE_PLUGIN_CACHE_DIR.

Also restore the .gitignore entry for one release cycle so installs still on
1.2.0 cannot commit a working-directory report, name the report location in the
command description, and note that uninstall drops the data directory without
--keep-data.
…ct two doc claims

Narrow the grant from ** to *: the plugin data directory is a single path
segment, so ** additionally pre-approved any ai-validation/ directory at
arbitrary depth under data/.

Name CLAUDE_CONFIG_DIR as the variable that relocates the tree the grant is
written against. CLAUDE_CODE_PLUGIN_CACHE_DIR governs plugins/cache/, not
plugins/data/, so it would not have moved the report.

Attribute --keep-data to `claude plugin uninstall`, the CLI subcommand that
documents it, rather than the interactive /plugin manager.
…ugin

A permission pattern allows partial-segment globs, so the plugin can be named
without pinning the marketplace: the trailing -* globs the install-id suffix.
The previous /*/ wildcard covered every installed plugin's ai-validation
directory.

Name CLAUDE_CODE_PLUGIN_CACHE_DIR alongside CLAUDE_CONFIG_DIR in the caveat.
It relocates the plugins root independently, so dropping it left the caveat
incomplete.
Claude Code checks file permissions against Edit(path) and Read(path) rules
only. A path rule written for Write is accepted, never consulted, and warned
about at startup, so both commands' mandatory final write was unapproved
despite the declared scope. /validate-ai carried the higher impact, running
non-interactively in CI where no prompt can be answered.

An Edit rule applies to every built-in tool that edits files, and allowed-tools
pre-approves rather than restricting availability, so the Write tool stays
callable under the Edit rule's path.

Drop Bash(ls:*) from both commands; neither body invokes ls.
…ll-id glob

Restoring Bash(ls:*) on both commands. Calling it unexercised was wrong: step 3
probes $BW_GH_ACTIONS_PATH/validate-ai/scripts, and ls was the only granted tool
that could resolve a path behind an environment variable. Dropping it cost a
permission prompt on the script-location lookup.

Drop the required hyphen from the install-id glob. A marketplace install yields
claude-config-validator-bitwarden-marketplace, but a local --plugin-dir load
yields the bare id, which claude-config-validator-* could not match, leaving the
mandatory write unapproved in exactly the case this change set out to fix.

Record the 2.1.210 floor the Edit-covers-Write behavior needs, cite where the
data-directory layout and --keep-data are documented, stop reusing the // rule
syntax as if it were part of the /tmp path, and drop the stale Write-grant
wording from the skill.
…gnore entry

Nothing writes that filename into a working directory any more: /validate-ai-local
writes under the plugin data directory and /validate-ai writes /tmp. Installs still
on 1.2.0 can surface the file until they upgrade, which is an accepted risk.

Note the Linux sandbox startup warning about glob patterns in permission rules, so
a user who sees it knows the report is still written.
…add a fallback

The stated reason for writing the data path literally was that substitution in
allowed-tools is documented for Bash rules only. That is true of the docs but not
of the CLI, which substitutes the whole allowed-tools value regardless of rule
type. The real reason is that substitution is guarded on the plugin having a
marketplace source, so a local --plugin-dir load can leave the variable unexpanded.

Give step 7 an explicit fallback for that case, pointing at the bare-id directory
the Edit glob already covers, and rule out falling back to the working directory.
@withinfocus
withinfocus merged commit 7748976 into main Aug 17, 2026
15 checks passed
@withinfocus
withinfocus deleted the validate-ai-local-plugin-data-reports branch August 17, 2026 15:23
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.

2 participants