Repository navigation
v1.18.2
1.18.2 — 2026-10-29
A second atomic audit (ten P0, ten P1) was run against 1.18.1 with every finding reproduced against real git / real hook processes before counting. Every P0 and every P1 is fixed below; the doc claims the audit falsified are now true in code, not just in prose.
- [P0] Repository-controlled
core.sshCommandcould execute during a review. The config scanner used\bcommand\s*=, which never matchessshCommand(camelCase:his a word char), sorepoRunsGitPrograms()returned no refusals while the allowlisted — and prompt-suggested —git remote show originran the repo's command twice before any network I/O. The scanner now reads config throughgit config --file <cfg> --no-includes --list -z(normalised keys,GIT_HARDENING_ARGS+ isolatedgitSpawnOpts) and refusescore.sshCommand,core.fsmonitor,core.pager,core.gitProxy,credential.helper,core.attributesFile,diff.external, and every livediff.*/filter.*driver; the regex path remains as a fallback when git is unavailable. The reviewer child process also getsGIT_CONFIG_*command-scope neutralisation (7 keys) so its owngitcalls cannot be steered by repo config. - [P0]
core.attributesFilewas never scanned. Git has three attribute sources; only the worktree.gitattributesand.git/info/attributeswere read, so a repo could pointcore.attributesFileat its own file and havegit log -pexecute a textconv (measured: 6 executions, scanner saw nothing). That file is now a third attribute source: in-repo regular files are scanned, out-of-repo/irregular/oversized targets are refused. - [P0]
MAX_ATTR_FILEShit inside the directory loop did not settruncated— the scan stopped silently instead of failing closed as its own comment and the 1.18.0 changelog promised. Now it refuses. (The audit's 4100-file repro was invalid — files namedaN.gitattributesare not attribute files and were never counted; the honest fixture is one.gitattributesper directory, and the regression uses that.) - [P0]
echocould exfiltrate host credentials.echowas exempt from path checking and a bare$VARwas not modelled as substitution, soecho $ANTHROPIC_API_KEY,echo ~/*,echo ../*were all allowed by the real hook (the child process inherits the host environment, andSECURITY.mdpromises no credential exfiltration).$VAR/$1/$?are now substitution and unquoted globs/~inechooperands are refused — quote-protection is tracked by the tokenizer, so the legitimate literal forms (echo 'price: $5',echo '*') still run, whileecho $ANTHROPIC_API_KEYandecho ~/*are denied. The tool description and SKILL.md say literal text only. - [P0]
trfile operands were unchecked (defense in depth: neither BSD nor GNUtrtakes FILE operands, so the audit's "arbitrary read" overstated it — but the pipe sink is now path-checked like every other sink). - [P0] The hook's Read branch failed open. When
realContainment()threw (repo root deleted/renamed mid-run, unreadable parent, symlink loop)relbecamenulland the check was skipped, soRead /etc/passwdpassed — while the Write branch denied. Read now denies on the same conditions, includingrel === null. - [P0] A hook that could not read its stdin exited 0, which the CLI reads as "no opinion" — the call ran unguarded. It now emits a deny decision and exits non-zero (
onFailure: "block"is the second line of defence). - [P0]
baseRefwas validated by two different validators.HEAD~1/feature@2passedindex.jsand were then silently dropped byprompt.js, so a fleet review reviewed a different diff with no sign of it. One exported validator is now the single source of truth; a ref the preflight accepts is the ref the prompt uses (range operators../...are refused instead of half-honoured), and the error text no longer recommends a value that gets discarded. - [P0]
scope=prwithoutprrendered "the GitHub pull request undefined" into the prompt and, on the native path, silently reviewed a different target. It is now refused before any paid work. - [P0] The followup path passed an empty denial set to
shellChainingNote(), so every command the guard had correctly blocked was reported as "RAN WITHOUT BEING BLOCKED — treat this repository as hostile". Followup now classifiespermission_denialsexactly like review, and reports its own allowlist gaps too. - [P1] The shipped SKILL.md confidence scale (0/25/50/75/100) contradicted its own
>= 80filter — only 100-point findings survived. Filter and tool description are now>= 75, matching the "real, important, will be hit" anchor. - [P1] Active JSONL journals were not actually protected:
activeSubagentFileswas written and never read, so the 20-file prune and the 2-hour sweep could delete a journal a live job still referenced. Both sweeps now skip active journals (and the run's own journal), and the set is cleared when the run settles. - [P1]
cross-cases' C7 assertion was vacuous (the array was passed in thecondslot), so "no allow rule is prefix-shadowed by a deny rule" could never fail. It now asserts the filtered length. - [P1] The P0 regression suite was marked
optional— deleting the only file that locks the P0 security fixes keptnpm testgreen. It is required again; a missing suite is a failure. - [P1] The test suite leaked real credentials into
$TMPDIR:cross-casesspawnedinit.mjswith the full host environment and never removed its temp tree. It now scrubsANTHROPIC_*, injects a sentinel token, proves the written config carries only that sentinel, and removes the tree on exit/crash/signal. - [P1] The log/guard directory trusted whatever was at
/tmp/cc-code-review.mode: 0o700only applies on creation, and on Linux/tmpis shared — a pre-created directory (or a symlink to an attacker's) received run logs andguard-settings-*.json, which claude reads at spawn time. Both paths now refuse symlinks and foreign ownership and force 0700. - [P1]
claude_review_envcould throw on a malformedANTHROPIC_BASE_URL(new URL()unguarded) instead of reporting it. Guarded and reported as unparseable. - [P1]
overridesnever reached consumers (npm only honours the root project's overrides), so the@hono/node-serverpin was inert for anyone installing the package. It is a direct dependency now. - [P1] Model substitution was blind to claude→claude downgrades:
sameModelFamily()returned true for anyclaude-*effective model, so a gateway silently running Sonnet as Haiku produced no⚠ MODEL SUBSTITUTION. Families (opus/sonnet/haiku) are compared when a model was requested. - [P1]
pipeline=native+ shadowed skill returned througherr()instead ofpreflightFail(), leaking the guard settings file and its in-flight mark on every such call. - Stale-process detection now has teeth. The hook exposes a hash of the policy tables (
CC_REVIEW_GUARD_INFO=1) and the pre-run self-test compares it with the server's in-memory policy; an upgrade without a host restart is refused instead of running two different policies — the "the guard self-test refuses to run" promise README/SECURITY made is finally true. - Audit corrections recorded (so the next pass does not re-chase them):
trhas no FILE operand in BSD/GNU coreutils (the guard change is defense in depth, not an arbitrary-read fix); the 4100-fileMAX_ATTR_FILESrepro was invalid;echo /etc/passwdprints a literal, not file contents — the real leak was wildcard/$VARexpansion. - Suites: shell-guard 238, audit-fixes 272, p0-regression 95, validation 16, cross-cases 139/139 (C1–C22), smoke PASS —
npm testgreen.