v0.3.6 — audit cuts
An over-engineering audit of the whole tree, and what it turned up. −234 lines of shipped code. Nothing you invoke changes except one flag-parsing fix.
claude plugin update guard-hooks@cc-agents-kit
claude plugin update context-handoff@cc-agents-kitThen restart open sessions.
Fixed — session-to-md swallowed mistyped flags
Its hand-rolled argument loop treated anything it did not recognise as the session id. So --tools none typed as --tool none did not complain — it silently exported a different session.
The flags are parsed with node:util's parseArgs now, which is strict: a flag it does not know is an error naming the flag, on one line rather than as a stack trace. The usage line also still named session-to-md.mjs, the path this script had before 0.3.1 moved it into bin/.
Honest note: this one is not a cut. With the error handling it is a line or two longer than the loop it replaces. It shipped because the audit's "use the stdlib" finding turned out to be hiding a real defect.
The opt-out contract was copied seven times
Every guard hook checks its CC_GUARD_DISABLE_* variable after draining stdin, never before — otherwise a disabled hook leaves the harness writing to a closed pipe and the tool call reports an error at exactly the moment you asked the hook to be off. The case that pins this feeds the hook a 200KB payload through a real shell pipe and reads the writer's PIPESTATUS, because Python's subprocess swallows BrokenPipeError and an ordinary test cannot see the bug.
That machinery was byte-identical in all seven suites, and the three blocking cases after it were byte-identical in four. It now lives in hooks/_optout.py. 324 lines of copies became 29 lines of calls.
staged-secret-guard's diff selection
The -C <path> expansion was spelled out at twelve call sites, and the diff selection was three branches by two sub-branches of one command differing only in ref and pathspec — now one condition and one call.
One call stays spelled out, and the suite is why: routed through the new wrapper, $! became the subshell's pid instead of git's, so the kill and wait around check-attr no longer reached it and a terminated hook would have orphaned the child. The case caught it on the first run.
Smaller
- CI listed the shell scripts to check twice, in two byte-identical heredocs. One step writes the list now, and asserts it is non-empty — a discovery that quietly matched nothing would otherwise leave both consumers passing on empty stdin, which is a gate that reads as green while checking nothing.
marketplace.jsondropsversion. Verified dead rather than assumed: the client records a version per plugin ininstalled_plugins.json, and only source, install path, and timestamp inknown_marketplaces.json. Nothing reads a marketplace-level version, and it had sat at0.1.0through five releases.- CLAUDE.md claimed a component thin enough to be effectively vendored upstream code does not belong here, while
context-budgetships at 87% verbatim and CREDITS.md says so. The rule now matches the practice: mostly-upstream is fine as a declared redistribution — measured overlap, copyright holder named, licence permitting it — and what is barred is upstream work presented as this repository's own.
Verification
Equivalence is the whole claim of a refactor, so it was measured rather than inferred from the suites going green:
- Ten fixtures covering every branch of the old diff-selection tree — staged, worktree, pathspec,
-a, with and without HEAD — return identical verdicts under both versions. - Moving each hook's opt-out check above its stdin read fails the drain case in all seven suites, with
writer=141. Deleting the check outright fails the first blocking case. No mutation was skipped for matching nothing.
Versions
guard-hooks 0.2.3 → 0.2.4, context-handoff 0.1.2 → 0.1.3. repo-gate is unchanged at 0.1.4.