fix(analyze-service): an alias could silently shadow a real index entry, and an unmarked client-sensitive file read as publishable - #362
Merged
Conversation
…ry, and an unmarked client-sensitive file read as publishable Four surgical repairs to the `/analyze-service` index schema, from an experiment that hand-authored a `process` entry and an `org` entry in the current schema to find where it breaks. The larger "generalize into subsystems" proposal was REJECTED on that evidence — no `type:`-driven sections, no dependency graph, no CLI. Spec/prose only; no code, no behaviour outside the command. 1. Addressing — the shadowing bug. Two defects were conflated. A slug can name two different KINDS of thing (`devrc/repo-cos` is both a code subsystem and the weekly ritual about it), and the old resolver matched filename *and* `aliases:` in one breath — so an alias silently won and a later real entry became unreachable, with no error. Now: an optional `<slug>.<kind>.md` filename used ONLY when disambiguation is needed; a two-tier resolver where an alias can never outrank a filename match; and >1 hit in a tier is a hard ambiguity ERROR that lists candidates instead of picking one. Bare `<slug>.md` resolves exactly as before — backward compatibility is stated as a hard requirement, and none of the 20 existing files is renamed or resolves differently. Also folds `_` -> `-` during normalization (the old rule kept `_`, so `image_ingestion` != `image-ingestion` while the index links `_`-spelled MEMORY.md slugs), keeping the pre-fold spelling in `aliases:`. 2. Sensitivity marking. New `sensitivity:` front-matter field, `client-confidential | personal | public`, with a FAIL-SAFE default: absent or unrecognized means client-confidential, never public. Not hypothetical — live entries carry a client bastion's public IP and SSH port, client hostnames, and a named client engineer. The no-remote / never-copy-into-a- public-repo rule is written down alongside it, pointing at 60e6d9d, which exists because this exact data class had to be scrubbed retroactively. Marking only; enforcement lands separately. 3. Front-matter repair. `repo:` -> `scope:`, and a scope may be a non-repo word (a ritual is owned by no repo; a client spans several). `<repo-slug>` derivation stays the default so nothing breaks; older files carrying `repo:` still read as `scope`. `namespace:` becomes OPTIONAL rather than removed — genuinely load-bearing for k8s infra, meaningless for the non-infra entries that were writing `n/a`. 4. Process-liveness convention. "Is this still being followed?" is THE question for a process subsystem, but the anti-bloat rule forbids persisting live status — correctly, for infra, where a kubectl is two seconds away. For a ritual there is no live probe. Convention: persist the DERIVATION METHOD and the stale-tell, never the current reading. Written as an application of the existing rule, not an exception to it. Byte budget: 12,460 -> 15,358 (+2,898). Offsetting evictions in the same commit — the recon-start index-read paragraph was stated twice nearly verbatim (kept the operational copy in step 1), the "Always re-derived live" tags on Lives at/Config duplicated the Provenance-honesty line below them, and the "Pointers, not copies" bullet restated the schema it sits under. Verification: the public-IP and client-hostname gates pass on this file, and were negative-controlled — injecting a routable IP into it makes test_no_public_ips fail with this exact path, so the green is a real read of this file rather than a scan that misses it. The full pytest suite fails identically (same 26, same names) at this branch and at the untouched base checkout, so nothing here is a regression; those 26 are ad-hoc-nix-shell toolchain gaps, not code. Scope note: deliberately does NOT touch scripts/analyze-service-index/, nix/home.nix or scripts/claude-hooks/ — PR #361 owns those and this file is not in its diff, so the two do not overlap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…it step" invited destructive git into an irreplaceable, unbacked-up store
`claude/commands/analyze-service.md` is the protocol an agent reads *before* writing
to `~/.claude/analyze-service-index/`. That store is now one git repo per scope
(verified: `<scope>/` is a repo on `trunk`, no remote; the store root is not a repo).
Three statements in the file predated that. Two were reworded earlier in this PR; the
dangerous one survived:
- "The write is local and final; there is no commit/worktree step (the file is
outside every repo)."
+ "The write is local and final — nothing leaves the machine: no remote, no push.
But it is not outside git; committing the scope repo is the store's own concern
(an out-of-band autocommit), never this command's, so write the file and run no
git command (Store safety)."
An agent told "outside every repo" has no reason to check for one before reaching for
the stash stack, a hard reset, a clean, or a checkout-- inside a store that is curated,
irreplaceable, unbacked-up and client-sensitive. `refs/stash` is repo-global and
concurrent sessions share it. Per RULES.md, a comment is a claim too — a false safety
comment is what leads a maintainer to delete the guard it describes.
Also fixed, same staleness class, found by sweeping the rest of the file:
- The earlier rewording ("outside every repo you work in" / "never lands inside a
cluster repo or devrc") was true but still only said where the store ISN'T, so it
left the same inference intact. The Location bullet now states positively that each
scope is its own remote-less git repo.
- "Lazy — nothing pre-created. ~/.claude/analyze-service-index/ may not exist" — the
store root exists today; scoped to "a scope dir or service file may not exist yet".
Adds a Store safety block mirroring the scope README's wording (the four destructive
git operations, no remote, no push, no copying into a public repo) so the two agree
rather than drift.
Wording is deliberately true both before and after the autocommit timer lands — that
timer is NOT deployed yet (`analyze-service-index-commit.timer` does not exist on this
host), so the file asserts a design, not a deployed state.
Byte discipline: this file loads on every run and had grown +2,898 (+23%) in this PR.
Net this round: 15,358 -> 15,349 (-9). Paid for by evicting the "NOT notable" list
(a verbatim restatement of "Bloat discipline" — one rule, one place), folding the
sensitivity bullet's handling rules into the new Store safety block, and compressing
prose without dropping a rule.
Nix-managed: takes effect only after `home-manager switch`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Four surgical repairs to the
/analyze-serviceindex schema, found by an experiment that hand-authored aprocessentry and anorgentry in the current schema to see where it breaks. The larger "generalize into subsystems" proposal was rejected on that evidence — this PR adds notype:-driven sections, no dependency graph, no CLI. Prose/spec only, one file.Deliberately does not touch
scripts/analyze-service-index/,nix/home.nixorscripts/claude-hooks/— #361 owns those, andclaude/commands/analyze-service.mdis not in its diff. Zero overlap.1. Addressing — the shadowing bug
Two defects were conflated:
devrc/repo-cosnames both a code subsystem and the weekly ritual about it.aliases:in one breath before declaring a miss — so an alias silently won and a later real entry became unreachable, with no error. Live example:bastion.mdclaims the aliasssh; a future realssh.mdwould have been shadowed by it.Fix:
<slug>.<kind>.md(repo-cos.process.md), kind ∈service | process | org | doc, used only when disambiguation is needed. A trailing dot-segment counts as a kind only if it is in that enum, so a dotted slug is unaffected._→-during normalization, keeping the pre-fold spelling inaliases:. The old rule (any char outside [a-z0-9._-]→-) preserved_, soimage_ingestion≠image-ingestion— while the index links_-spelled MEMORY.md slugs throughout.Backward compatibility is a hard requirement and is stated as one. None of the 20 existing files needs renaming, and none resolves differently:
_, so the fold changes nothing about what they match —image-ingestion.md,spine-controller.md,civitai-dp-prod.mdresolve exactly as before.pghero.mdcarriesaliases: [pg-hero, pg_hero]; both fold topg-hero, both still resolve, andpg_heronow also matches when typed against a--spelled entry.2. Sensitivity marking
New
sensitivity:front-matter field —client-confidential | personal | public— with a fail-safe default: absent or unrecognized meansclient-confidential, never public, andpublicis a deliberate operator claim a recon run may never infer.This is live, not hypothetical:
bastion.md:10carries a client bastion's public IP and SSH port,orchestration.mdnames a client engineer by handle, and severalaliases:lists carry client hostnames. The spec now writes down that the store must never gain a git remote and that none of its content may be copied intodevrc(PUBLIC) or any other public repo, PR body, issue, gist or commit message — pointing at60e6d9d, which exists because this exact data class had to be scrubbed retroactively.Marking only. Enforcement lands separately, on top of #361.
3. Front-matter repair
repo:→scope:, and a scope may be a non-repo word — a ritual is owned by no repo, a client spans several.<repo-slug>derivation stays the default so nothing breaks; older files still carryingrepo:read asscope.namespace:becomes optional rather than removed — genuinely load-bearing for k8s infra entries, and meaningless for the non-infra test entries that were reduced to writingn/a. Omit it rather than writingn/a.4. Process-liveness convention
"Is this still being followed?" is the question for a process subsystem, but the anti-bloat rule forbids persisting live status — correctly, for infra, where a
kubectlis two seconds away. For a ritual there is no live probe.Convention: persist the derivation method and the stale-tell, never the current reading. From the experiment — liveness of a weekly ritual was derived from an exclusions-file mtime vs. the timer's last fire; that method is the durable knowledge, and the answer it returned ("last followed 2026-08-01") is live status exactly like a pod count and stays forbidden. Written as an application of the existing rule, nested under it, not an exception to it.
Byte budget
12,460 → 15,358 (+2,898). Evictions taken in the same commit to offset:
## Pointersschema it sits under.Verification
test_no_public_ips.py+test_no_client_hostnames.py: 33 passed on this branch.test_no_public_ipsfail naming this exact path and line, so the green is a real read of the changed file, not a scan that misses it. Reverted; file restored byte-identical.scripts/testssuite: 26 failed / 1388 passed on this branch and the identical 26 failures, by name, at the untouched base checkout. They are ad-hoc-nix-shelltoolchain gaps (nologrotateon PATH, no opencode engine, run-tests preconditions harness), not regressions from this change.Takes effect only after a
home-manager switch— this path is a read-only nix-store symlink at~/.claude/commands/, and it also ships to opencode.🤖 Generated with Claude Code