Skip to content

v0.3.6 — audit cuts

Choose a tag to compare

@AndrewDongminYoo AndrewDongminYoo released this 25 Aug 11:11
· 44 commits to main since this release
3fe6b7e

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-kit

Then 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.json drops version. Verified dead rather than assumed: the client records a version per plugin in installed_plugins.json, and only source, install path, and timestamp in known_marketplaces.json. Nothing reads a marketplace-level version, and it had sat at 0.1.0 through five releases.
  • CLAUDE.md claimed a component thin enough to be effectively vendored upstream code does not belong here, while context-budget ships 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.