Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/claude-config/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "claude-config",
"version": "0.30.0",
"version": "0.31.0",
"description": "Seven configuration-health skills (plus setup) for a repo's Claude Code configuration: audit (settings.json / .mcp.json / hooks / plugins / permissions drift), audit-automation-gaps (evidence-gated verdicts on automation gaps), audit-permission-grants (allow-rule / allowed-tools grants for auto-mode durability and portability), audit-instructions (locally-owned instruction surfaces vs current model capability — proposes removals/rewrites of instructions the model no longer needs, and detects cross-surface instruction conflicts), audit-prompting-postures (the additive lane — posture guidance the prompting guide says a component's purpose needs but the component does not carry), audit-pass (one coordinated, ordered, resumable pass over a named target — three-scope inventory, run-time-derived exclusion set, stable finding identity, suppression memory, resume, one human gate — delegating every check to the plugin that owns it), and unhobble (the empirical bare-baseline experiment: reversibly strip a repo's standing instructions, log real stumbles against the current model, re-add only what evidence earns).",
"author": {
"name": "Melodic Software",
Expand Down
68 changes: 68 additions & 0 deletions plugins/claude-config/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,74 @@
All notable changes to the `claude-config` plugin are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning.

## [0.31.0]

`audit-pass` changes what it does on two target classes, which is why this is a minor: a target that is
not a git repository is now refused, and a target at or above `~` now gets the self-exclusion and
disclosure that only `--report-to` runs used to get. The three fixes are independent — the refusal does
**not** subsume the containment work, because the class that falsified the containment claim is a *git*
repository rooted at or above `$HOME`, which a dotfiles repo is.

### Fixed

- **`audit-pass` claimed its report lands "outside any target repository", and that is false by
construction.** `${CLAUDE_PLUGIN_DATA}` resolves to `~/.claude/plugins/data/{id}/`
([plugins reference](https://code.claude.com/docs/en/plugins-reference), verified 2026-08-11), and no
documented setting relocates it — so the default report path is *inside* any target at or above `~`.
The sentence was true for an ordinary repo under `C:/Projects/…` and stated as a universal, while a
whole reachable target class falsifies it. It is now stated as what it is: outside a target below `~`,
inside one at or above it. That claim was load-bearing for the read-only contract's headline property
and for assertions 2.1 and 2.4.
- **And the machinery that exists for exactly that situation was gated on the wrong thing.** Name-based
refusal, the pre-write exclusion entry, the operator disclosure, and the digest exclusion were all
written under the `--report-to` bullet. The flag is one way a report path ends up inside the target;
it was never the definition. The governing condition is now the predicate `report_path ⊆ target_root`,
evaluated against the resolved path on every run, with `--report-to` demoted to one instance of it.
Class 4 of the exclusion set is stated as that predicate rather than as a two-item list, so a member
added later inherits the rule instead of needing someone to remember the list.
- **The consequence this retires:** a run against a dotfiles repository, or against `~` itself, wrote
its report into its own scan set with no exclusion entry and no disclosure, and then reported
`indeterminate` on its own determinism gate — every time, purely for doing what it was asked. The
digest exclusion in `determinism-tiers.md` is re-keyed on containment for the same reason.
- **Assertions 2.1–2.4 are re-scoped on containment, and 2.6 is added.** 2.1 ("`git status --porcelain`
is empty") was falsified by the same class as 2.4, not just 2.4. 2.6 is the default-path twin of the
redirect disclosure: a run against a target at or above `~` with no `--report-to` discloses that its
report path is contained and names it, so a contained write is never silent.

### Changed

- **`audit-pass` refuses a target that is not a git repository.** `SKILL.md` called `target` "the git
repository to audit" while the gate required only that it "resolve to the active project root" — so a
non-git directory passed, and the contract then had no branch for it in five places: the state key has
a no-remote fallback and no no-git one; the scan baseline is defined as HEAD plus the state digest;
Class 3 exclusion derives worktrees from `git worktree list` and, unlike Class 1, is given no fallback;
assertion 2.1 is stated over `git status --porcelain`; and — the one that is a permanent capability
loss rather than a missing derivation — suppression is enacted only by the team layer, which is the
*tracked* layer, so no suppression is ever enactable on such a target. An operator could accept a
finding there and have the acceptance silently fail to persist, forever.

**This closes a target class, deliberately.** The alternative was specifying all five branches, which
obliges the contract to promise a capability it cannot deliver on that class. The refusal reuses the
existing channel — non-zero, before Phase 0, naming the path and the reason — rather than minting a
new exit code, and it states the cost out loud, naming the suppression consequence in particular, so
it does not read as an arbitrary restriction.

**It names the directory even when resolution produced nothing**, which is the case the refusal is
most for: on a bare invocation with no `${CLAUDE_PROJECT_DIR}`, the documented default is
`git rev-parse --show-toplevel`, and that fails outside a repository — leaving no resolved root to put
in the message. The diagnostic falls back to the current directory so the refusal can say what it
refused. That fallback is for the message only and never becomes a target.

### Added

- **Eval 30 `non-git-target-is-refused-not-half-specified`**, and evals 1 and 20 rewritten. This is the
row that had to land first: eval 1's expectation graded "writes the report under `CLAUDE_PLUGIN_DATA`
**rather than** into the scan set", and eval 20's graded the digest exclusion as a `--report-to`
property — so the suite passed a defect-exhibiting run and would have failed the corrected one. Eval 1
now grades that the containment predicate is *evaluated*, and that where it holds the path is recorded
before the write and disclosed; eval 20 grades that the exclusion is keyed on containment and not on
the flag. Reading all 29 shipped cases at HEAD found no others carrying the premise.

## [0.30.0]

Two graded outputs move, which is why this is a minor rather than a patch: a baseline deny finding can
Expand Down
11 changes: 10 additions & 1 deletion plugins/claude-config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,13 @@ and one human gate per run. Findings report in three tiers — derived (exact eq
judged (a stability tolerance whose violation fails the run's self-check), delegated. `/doctor` is an
operator handoff, never a dispatch, because it is interactive.

**The target must be a git repository**, and one that resolves to the active project root. Both halves
are refused non-zero rather than reinterpreted. The git requirement is not incidental: the state key,
the scan baseline, the worktree exclusion, and the top read-only assertion are all defined over git,
and suppression is enacted only by the *tracked* layer — so on a non-git target no suppression would
ever persist. Audit such a directory by opening it as a repository, or through the delegated skills
directly.

```shell
/claude-config:audit-pass # read-only pass over the current repo
/claude-config:audit-pass --opinion # include the default-off OPINION-tier checks
Expand Down Expand Up @@ -181,7 +188,9 @@ automatically. If you used `/claude-config-audit:memory-health`, install it expl

No `userConfig`. One tracked consumer-project file — `audit-pass`'s suppression record, above.
Persistent plugin state: `audit-pass` writes its run reports and manifests under
`${CLAUDE_PLUGIN_DATA}`, outside any target repository, so a run never writes into its own scan set.
`${CLAUDE_PLUGIN_DATA}`, which resolves under `~` — outside a target below the home directory, and
inside one at or above it. A run never *scans* what it wrote: where the resolved report path is
contained in the target, the run excludes that path before writing and says so.
Network: `audit` fetches official docs pages and each registered marketplace's `marketplace.json`
from `raw.githubusercontent.com` (read-only; a failed fetch degrades to SKIP).

Expand Down
52 changes: 47 additions & 5 deletions plugins/claude-config/skills/audit-pass/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,56 @@ Parse `$ARGUMENTS`:
**delegated** interfaces — each would have to accept and honor a target root — and belongs to
those skills rather than this one. The argument itself survives because the state key, the lock,
and the report are already keyed on the resolved root.

**The gate enforces both halves of that first sentence: the active project root, *and* a git
repository.** A `target` that is not inside a git repository is refused the same way — non-zero,
before Phase 0 does any work, naming the path and the reason, writing nothing.

**Name the directory, not an empty string.** In the case this refusal is *for*, the default
resolution above produces nothing: with no explicit `target` and no `${CLAUDE_PROJECT_DIR}`,
`git rev-parse --show-toplevel` fails outside a repository and there is no resolved root to report.
So for the diagnostic only, fall back to the current directory and name **that** — a refusal that
cannot say which path it refused is barely better than a silent one. The fallback is for the message;
it never becomes a target.
Requiring only "the active project root" let a non-git directory through into a contract with no
branch for it, and the run then went quiet in five places rather than one:

- the state key (§3) has a no-**remote** fallback and no no-**git** one, and "canonicalized repo
root" is undefined without a repository;
- the scan baseline is *the target's HEAD commit and the run's state digest*, and HEAD does not
exist;
- Class 3 exclusion derives worktrees from `git worktree list`, and unlike Class 1 it is given no
fallback;
- assertion 2.1 is stated over `git status --porcelain`, so the top read-only assertion is
unevaluable;
- and — the one that is a permanent capability loss rather than a missing derivation — **only the
team layer enacts a suppression**, and the team layer is the *tracked* layer. With nothing
tracked, no suppression is ever enactable on such a target, so an operator could accept a finding
and have the acceptance silently fail to persist, forever.

**The refusal says that cost out loud** rather than reading as an arbitrary restriction, and it names
the suppression consequence in particular. Refusing closes a target class deliberately; it is not a
side effect. The alternative — specifying all five branches — was considered and rejected, because
the last of them obliges the contract to promise a capability it can never deliver on that class.
A non-git directory is audited by opening it as a repository, or by the delegated skills directly.
- **`--fix`** — the explicit mutation override. Absent, the pass writes nothing into the target.
- **`--opinion`** — run the `OPINION`-tier checks the delegated catalogs declare default-off.
- **`--resume`** — resume the most recent incomplete run for this target's state key.
- **`--report-to <path>`** — redirect the report into the target tree. **The redirecting run adds
that path to its own exclusion set before writing** — not only for later runs, or the two runs'
derived sets could not be equal — and says so in its output. The destination is accepted only if it
is an `audit-pass`-owned report or a new path that is **not a recognized instruction surface**;
- **`--report-to <path>`** — redirect the report into the target tree. The destination is accepted only
if it is an `audit-pass`-owned report or a new path that is **not a recognized instruction surface**;
anything else is refused non-zero, naming the file. Refused on name rather than on existence,
because `--report-to CLAUDE.md` against a repo that has none would *create* a live instruction
surface out of a JSON report and then hide it from every later scan.

**The self-exclusion obligation is not this flag's.** It belongs to the predicate
`report_path ⊆ target_root`: **any** run whose resolved report path is contained in the target adds
that path to its own exclusion set before writing — not only for later runs, or the two runs' derived
sets could not be equal — and says so in its output. `--report-to` is one way containment arises. The
**default** path is another, because `${CLAUDE_PLUGIN_DATA}` resolves under `~` and is inside any
target at or above it. Full statement in
[reference/report-location-and-schema.md](reference/report-location-and-schema.md) §2 and
[reference/exclusion-set.md](reference/exclusion-set.md) Class 4.

## Phase 0 — Resolve, key, lock

Resolve the target root, compute the state key, and take the lock posture for the mode — read-only
Expand Down Expand Up @@ -350,4 +389,7 @@ recalibration.
defect this skill's whole shape exists to avoid.
- Never reads another plugin's files. Cross-plugin cooperation is invocation only.
- Never edits managed policy or a user-scope file, in any mode.
- Never writes into its own scan set without `--report-to`, and never scans what it wrote.
- Never scans what it wrote. Where its resolved report path is contained in the target — by
`--report-to`, or by `${CLAUDE_PLUGIN_DATA}` resolving under `~` for a target at or above it — the
path is excluded before the write and the containment is disclosed. Never silently.
- Never audits a target that is not a git repository. It refuses, and says what the refusal costs.
Loading