Repository navigation
fix: eleven audit findings, and the standing cost measured and cut - #45
Merged
Merged
Conversation
… in the workspace Eleven findings from an audit of the CLI and the hook layer, each reproduced before it was fixed. The three that were measured against a built binary: - `--help` exited 1 on all 52 flag-parsed subcommands, so an agent branching on the 0/1/2 contract read the help it asked for as a command that could not run. - A flag after a positional was swallowed as prose: `notes add --tag gotcha "text" --path x` recorded no path and wrote the flag into the note, exit 0. parseFlags now alternates between peeling positionals and parsing flags, and honours a bare `--`. - `scc patch append ../outside.md` wrote to a file outside the repository and exited 0. artifact.Resolve now requires the resolved path to sit inside the workspace, symlinks evaluated on both sides so a macOS temp dir still passes. The hook layer: - core.hooksPath was joined literally, so a configured `~/.githooks` became `<repo>/~/.githooks` and git read the real one. rev-parse --git-path already answers both halves. - A shebang with flags (`#!/bin/sh -e`) was called non-POSIX, so --force refused an ordinary husky hook. The interpreter is read out of the line now. - Stop ran the attribution validator, whose finding needs a rebase rather than an edit — a line that can never clear on a stage where speaking is a continuation. It also reported findings in files this branch never touched. Both are out: `validate.WithoutAttribution`, and a filter against `git.Changed`. - Stop synced the symbol graph on every turn. It now compares the graph's mtime against the files this branch changed and skips when nothing is newer. - pre-push ran the full gate on `git push --delete` and on a push carrying nothing. git says which on stdin, and that is now read. - commit-msg assumed `#`, so a repository setting core.commentChar read git's template as message and scanned the whole verbose diff. - `scc hooks run <stage>` blocked forever on a terminal, which the usage text invites people to do. - The undelivered-work line fired from the first commit of a unit of work, asking for a push mid-task on a stage that continues the conversation. It now waits for a clean tree. - A settings entry was claimed by substring, so `my-scc hooks run stop` read as scc's own. Output, where an agent pays per byte: `--json` is compact off a terminal (`map tasks --json` 2198 -> 1558 bytes) and a clean `scc validate` prints one line instead of twelve. `--no-verify` is renamed `--no-seal-check`, since git spells "skip the hooks" the same way and this skipped a plan's seal check.
…the review agents The standing cost of a scaffolded Claude Code workspace, measured rather than assumed: the entry file plus 15 rules came to 47KB preloaded into every request of the session, while CLAUDE.md still said nine rules and ~26KB. Nothing about the methodology changes here. What changes is when three of its files arrive. `specs.md`, `tasks.md` and `knowledge-base.md` carry a `paths:` header, so the harness loads them when the agent touches the tree they govern rather than at session start. That is 10.4KB off every request, about a quarter. The bar for scoping a rule is mechanical and tested: a validator has to report what the rule prevents. A scope trades "in context before the decision" for "in context when the file is open", and that is survivable exactly when something else catches the result — a malformed EARS line, a task with no methodology annotation, a broken wikilink are all exit 2 before the commit. The rules that look most scopable are the clearest refusals: artifacts.md would scope to `plans/**` and its instruction is never open the plan, so it would only load to say the thing it prevents has happened. The skill descriptions were the unmeasured half. A body is paid once by the run that invokes it; a description is paid by every request whether the skill runs or not. Eight came to 4087 characters, mostly enumerating findings the validator already names by rule — 2084 now, with a budget test so they cannot creep back. And the two review agents become `scc-code-review` and `scc-security-review`. Claude Code ships its own under the unprefixed names, so delivery.md naming them in prose resolved to whichever the harness picked — a collision nobody would ever see, because both produce a review and the one that ran is the one that answers. `scc update` turns the old files into deletes or orphans on its own. Also corrected: the stack skill claimed `scc validate` reads go.mod and package.json, where it reads seven manifests. Preloaded, Claude Code, fresh workspace: 52388 -> 40178 bytes.
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.
An audit of the CLI, the hook layer, the skills and the rules. Every bug below was reproduced against a built binary before it was fixed; the token numbers are measured on a freshly scaffolded Claude Code workspace.
Bugs
Reproduced against the binary:
--helpexited 1 on all 52 flag-parsed subcommands. An agent branching on the 0/1/2 contract reads the help it asked for as a command that could not run.scc notes add --tag gotcha "text" --path internal/xrecorded no path, wrote--path internal/xinto the note body, and exited 0. Misfiling a record while reporting success is worse than rejecting it.scc patchwrote outside the workspace.scc patch append ../outside.md L3 --text Xmodified a file outside the repository and exited 0. An artifact address reaches scc from a file the agent read as readily as from the user, so the boundary is inartifact.Resolverather than in one handler.map showhad the read half of the same hole.Found by reading:
core.hooksPathwas joined literally, so a configured~/.githooksbecame<repo>/~/.githooks: scc wrote three hooks where git never reads them.#!/bin/sh -emade--forcerefuse an ordinary husky hook.git push --deleteand on a push carrying nothing. git says which on stdin and nothing read it.#, so a repository settingcore.commentCharhad git's own template read as message and the whole verbose diff scanned for signatures.scc hooks run <stage>blocked forever on a terminal, which the usage text invites.my-scc hooks run stopread as scc's own.The standing cost
The entry file plus 15 rules came to 47KB preloaded into every request of the session.
CLAUDE.mdstill said nine rules and ~26KB, and had said so since the set was that size.Nothing about the methodology changes here. What changes is when three of its files arrive:
specs.md,tasks.mdandknowledge-base.mdcarry apaths:header, so the harness loads them when the agent touches the tree they govern.The bar is mechanical and tested: a rule may be scoped only where a validator reports what it prevents. A scope trades "in context before the decision" for "in context when the file is open", and that is survivable exactly when something else catches the result. The rules that look most scopable are the clearest refusals —
artifacts.mdwould scope toplans/**and its instruction is never open the plan, so it would only ever load to say the thing it prevents has happened.The skill descriptions were the unmeasured half: a body is paid once by the run that invokes it, a description by every request whether the skill runs or not. Eight came to 4087 characters, mostly enumerating findings the validator already names by rule.
map tasks --jsonscc validate--jsonis compact off a terminal and indented on one, the same testrendermakes about color.Naming
The review agents become
scc-code-reviewandscc-security-review. Claude Code ships its own under the unprefixed names, sodelivery.mdnaming them in prose resolved to whichever the harness picked — a collision nobody would ever see, because both produce a review and the one that ran is the one that answers.scc updateturns the old files into deletes or orphans on its own.--no-verifybecomes--no-seal-check. git spells "skip the hooks" the same way, and this skipped a plan's seal check while every validator still ran.Verification
go build,gofmt -l,go vet,golangci-lint run(0 issues) and the full suite are green.-racecould not run on this machine — no gcc for cgo — so CI is the first place it does.Smoke-tested end to end on a fresh workspace: init, the three git hooks firing on a real commit, the three harness stages,
pre-pushskipping a deletion and running on a real ref, and the Stop stage silent on a dirty tree and speaking on a clean one with an unpushed commit.