Repository navigation
Releases
v1.19.0
1.19.0 — 2026-11-03
Portable distribution: the repository is now an Agent Plugins v1.0.0 package, and the bundled code-review skill is harness-neutral. The review pipeline, the shell guard and the report contract are unchanged in behaviour — the embedded pipeline text is the same procedure, rephrased to speak in capabilities instead of Claude Code tool names.
- Agent Plugins v1.0.0 package. The repo root is a plugin root:
plugin.json(closed manifest),mcp.json(closed stdio config with${PLUGIN_ROOT}placeholders) andskills/code-review/SKILL.mdin the fixed discovery location. The skill moved fromassets/skills/code-review/toskills/code-review/; the server, pack-test, p0 regression and the fix-mode self-review globs follow the new path. A conformant client can load the skill and the MCP server from one directory; component failures stay non-fatal per the spec. - Harness-neutral skill.
skills/code-review/SKILL.mdis now a portable Agent Skills skill: frontmatter gainslicense/compatibility/metadata; the body speaks in capabilities (fan-out via "your harness's subagent/task tool", sequential fallback, a Harness notes section mapping tool names) and keeps the cc-code-review shell contract as the default read-only briefing, copied verbatim when the MCP run prompt supplies one. The report contract block, the 5 briefs, the three phases, the 0-100 scoring and the >=75 filter are unchanged —prompt.jsstill extracts the report block from this file, so there is exactly one copy. - New
plugin-testsuite (offline, free): validatesplugin.json(closed schema, canonical$schema, name/version/license constraints),mcp.json(closed top-level, same spec version, stdio variant rules, reserved env names,cwdforms) and every discoveredSKILL.md(Agent Skills frontmatter, name↔directory, description bounds, body limits, report-contract presence). Added tonpm test(now 8 suites). - README (both languages): cross-harness registration (ZCode one command; Claude Code / Cursor / any stdio client JSON), a new "use it as an Agent Plugin / Agent Skill" section, corrected skill paths, and the inline-prompt size (~12KB).
- No functional change to the review pipeline;
npm testgreen (8 suites).
v1.18.10
1.18.10 — 2026-11-03
Docs-only: no code change. Fixes two README errors and brings the GitHub page in line with the release line.
- README duplication. The Chinese README's English abstract read "or runs a fleet pipeline or runs a fleet pipeline with the same three-phase structure" (typo introduced in 1.18.4); the duplicated clause is gone.
- The npm-package claim is gone. Both READMEs told users they could "install the npm package" —
cc-code-reviewwas never published to the npm registry (npm view cc-code-review→ 404), so the supported install is cloning this repository. The tarball note (scripts/init.mjsships since 1.18.5) went with it. - Release badge + header links. Both READMEs gain a Release badge (now meaningful: every tag has a GitHub Release) and header links to CHANGELOG.md, the docs index and the Releases page.
- Log directory wording is cross-platform.
%TEMP%\cc-code-review\run-*.logdescribed Windows only; both READMEs now say "system temp dir / cc-code-review/run-*.log (Windows: %TEMP%)". - No functional change and no test change;
npm teststays green (7 suites).
v1.18.9
1.18.9 — 2026-11-01
No functional change. This release closes the gap between main and the release line after 1.18.8 and fixes one time-dependent test.
- Test: the codeDrift stale assertion no longer depends on the checkout's age.
scripts/audit-fixes-test.mjsassertedcodeDrift(now - 86_400_000).stale === true, which only holds while the checkout's source files are newer than "one day ago": a working copy older than a day failed the suite (317/318) even thoughcodeDrift()was right. The test now injects an epoch clock (codeDrift(0)), so the simulated process start precedes any plausible source mtime. No production code changed. - Docs corrections that landed on
mainafter 1.18.8: the 1.18.8 changelog wording, the round-8 report counts, and its dev+ino identity note (docs/04-test/20-fix-round8-v1.18.8.md). - Docs index fixed:
docs/README.mdwas missing the round-8 report entry (20) and still declaredversion: v1.18.7; both are corrected, and this release's report (21) is listed. - Suite counts refreshed in README.md / README.en.md / CONTRIBUTING.md to the actual numbers: shell-guard 327, audit-fixes 318, p0-regression 256, validation 31, pack 52, cross-cases 156/156 (27 cases defined, 26 run for free by default; C9 is paid).
- Suites: shell-guard 327, audit-fixes 318, p0-regression 256, validation 31, pack 52, cross-cases 156/156, smoke PASS —
npm testgreen (7 suites). - Release line:
v1.18.8stays at its release commit (7cbbfca);v1.18.9marks this docs/test-only release. No behaviour change.
v1.18.8
1.18.8 — 2026-10-30
Fixes a regression introduced by 1.18.7: the self-review pipeline protection was scoped by PATH SHAPE, so fix mode could not edit src/*.js in any repository.
- [regression] Pipeline write protection is identity-scoped. 1.18.7 added
^src\/[^/]+\.js$(and the glob**/src/*.js) toPROTECTED_WRITEto close R-2 — but the hook and the server always load from the package install directory, so a FOREIGN repository'ssrc/index.jsis ordinary source and editing it changes nothing. The rule therefore denied fix mode the one thing it exists for in ordinary projects (src/app.jswas refused). The pipeline patterns now live inSELF_REVIEW_PIPELINEand are applied only when the reviewed tree IS this package (isSelfReview()compares the filesystem identity of the package root andCLAUDE_REVIEW_REPO_ROOT); the permission-engine globs follow the same flag (fixDisallowedTools({ selfReview })). A self-review still refuses the pipeline files (manual edit by design); a foreign repo keeps them writable, and the repo-agnostic classes (CI/hook config, secrets,.git) are unchanged. isSelfReview()compares the filesystem identity (dev+ino) of the package root and the reviewed root, not path spellings: an alternate path or bind mount of the same tree is still a self-review (a realpath string compare would let a write through that path disarm later runs). A relative or empty root is never a self-review.- Tests: shell-guard adds
isSelfReview()+ table-separation assertions (including empty/relative roots and a symlink to the package); p0 asserts foreign repos allow the pipeline names while a self-review denies them (live hook process, both layers), and the cross-layer test now checks both scopes. - Suites: shell-guard 327, audit-fixes 318, p0-regression 256, validation 31, pack 52, cross-cases 156/156, smoke PASS —
npm testgreen (7 suites). - Behaviour change: in fix mode,
src/*.js/scripts/init.mjs/scripts/lib/**/assets/skills/**are writable again in foreign repositories; they stay read-only when reviewing this package itself. Restart the MCP host (policy fingerprint changed).
v1.18.7
1.18.7 — 2026-10-30
Closes the re-check of 1.18.6 (R-1/R-2/R-3/N-1) and the final audit of PR #5/#6 (1 P1 + 6 P2 + 5 P3). No new feature; behaviour changes at the end.
- [P1]
find --files0-frombypass. GNU find's historical long options are single-dash (-files0-from), and the new abbreviation matcher only applied to--table entries:find -files0-fromwas denied whilefind --files0-from/find --files0were allowed and dumped a host file's text into stderr (and the report). The deny table now lists both spellings anddenyReasonIn()matches a single-dash long name against both the-and--word forms. - [R-1]
find -D/-O. The guard skipped the token after them as "their value", but-Dtakes debugopts (letters/commas) and-Oa level (digits); any other token is a START PATH.find -D /etcandfind -O /etctherefore enumerated a host directory on GNU find. Only a value-shaped token is consumed now. - [R-2] Self-review protects the whole guard/spawn pipeline.
PROTECTED_WRITEcovered only the hook and shell-guard, while editingsrc/index.js(spawns the reviewer),src/claude.js(builds the child git env),src/prompt.js(renders the permission rules) orsrc/skill.js(writes the installed skill) disarms every later run just as effectively.src/*.js,scripts/pre-tool-hook.js,scripts/init.mjs,scripts/lib/**andassets/skills/**are protected in both layers; ordinary source and docs stay editable. Fixing this package in fix mode is a manual-edit job by design. - [N-1] The hook fails closed on a payload it cannot judge. Malformed JSON, empty stdin, a non-object or a missing
tool_nameused toexit(0)(allow), contradicting the hook's own contract; all of them deny now. A parsed payload naming a tool the hook does not police still passes — those are governed by the server allowlist and denying them would break every run. - [R-3] Runtime floor, CI matrix and badge agree. Node 18 is EOL:
enginesis>=20, the CI matrix is 20/22/24 (committed as.github/workflows/ci.yml), both READMEs say ≥20, and a pack-test assertion fails if the three drift apart. - P2 refactors (from the PR #5/#6 audit): the redundant
tildearray is gone (derived fromunquoted);writeGuardSettings()reusesensureOwnedPrivateDir()instead of a second copy;GIT_HARDENING'senv/argsbooleans collapse to onehardenflag (they could never legitimately differ — the drift that produced P0-1);looksLikeLaunchAck/launchAckBodyshare oneackShape(); the hook computes containment once instead of callingrepoRelative()and thenisInsideRepo()on the same path. postToPRrequires a fully-qualified PR selector. A bare number (pr: "123") is resolved byghagainst the reviewed repository's own remote — and that repository is untrusted content, so itsoriginwould decide where the summary comment is published.owner/repo#Nor a PR URL pins the target to what the caller named; reading a PR by bare number stays allowed (that is the documentedscope=prbehaviour).- P2/P3 single-source honesty: the prompt.js comment no longer claims the hook imports the glob table (it never did) — the two layers are kept aligned by a cross-layer test that asserts, for 21 protected paths, hook DENY and permission-glob coverage; the
.pre-commit-config.ymlspelling was added to the globs;GIT_HARDENINGandreportContractSource()are no longer dead exports (the table is asserted by the self-test, a degraded report contract surfaces as a run warning). - Docs: README test counts refreshed (shell-guard 320, p0 238, audit-fixes 318), the
baseRefrow no longer says illegal characters are ignored (they are rejected), the English README gains the 1.18.5/1.18.6/1.18.7 security sections it was missing, and the audit documents carry the required frontmatter. - Suites: shell-guard 320, audit-fixes 318, p0-regression 238, validation 30, pack 52, cross-cases 154/154, smoke PASS —
npm testgreen (7 suites). - Behaviour changes:
find --files0-from/--files0andfind -D|-O <path>are refused; fix mode can no longer edit this package's ownsrc/*.js/ hook / init / lib / assets; malformed hook payloads deny; Node 20+ is required; restart the MCP host (policy fingerprint changed).
v1.18.6
1.18.6 — 2026-10-30
Closes the residuals left by 1.18.5 (audit P2-1, P2-3, P2-22 and the documented P1-4/P1-13 gaps). No new feature; the behaviour changes are listed at the end.
- One git-hardening table (P2-1). The child-env overrides, the
-cargs for the server's own git spawns and the scanner's exec-key list were three hand-written tables and had already drifted: a key added to-calone never reached the claude child — exactly the class that produced theprotocol.ext.allowRCE. They are now generated from a singleGIT_HARDENINGtable, and a module-load assertion refuses to start if a key is protected for the server/scanner but not for the child. The child env grew from 9 to 16 pinned keys (the editor/diffFilter/gpg/merge/gitProxy/askPass/sequence keys were-c-only before). - One containment primitive (P2-3). Five copies of the realpath/containment logic had diverged (the hook refused the repo root, the guard allowed it).
realpathNearest,repoRelativeandisInsideRepo(root, token, {allowRoot})now live insrc/shell-guard.js; the hook, the guard and the repository-metadata reader all call them. - Recursive scans that defeat
.gitignore(P1-4 residual).rg --hidden,--no-ignore*,-u/-uu/-uuu,--unrestricted, and recursivegrep -r/-Rrooted at the repository (or with no path) are refused: those forms read ignored secret files (.env, keys) and print them into the transcript. Scoped searches (rg -n x src,grep -rn x src) stay allowed. package.jsonEdit install hooks. The hook checked onlyWritecontent; anEdit/MultiEditcould addscripts.postinstallone line at a time. Both tools are checked now (metadata edits stay allowed).- Policy fingerprint covers rule code (P1-13 residual). The fingerprint hashed the tables but not the inline rules, so an upgrade that changed
segmentProblem/dotGlobProblem/isInsideRepowithout touching a table passed the stale-policy check. The in-memory source of those functions is now part of the policy identity. - Fail-closed scanner. A repository whose
.git/configgit itself cannot parse is refused (it used to fall back to a line scan while git commands in that repo fail anyway), and an attribute enumeration that cannot run (e.g. a corrupt index) is refused instead of noted — attribute files under the prunednode_modules/would otherwise be unverified. - Lockfile registry (P2-22). All 94
resolvedtarball URLs pointed atregistry.npmmirror.com; they now point atregistry.npmjs.org(same paths and integrity hashes), so CI and consumers no longer depend on a third-party mirror. - Suites: shell-guard 298, audit-fixes 314, p0-regression 206, validation 30, pack 48, cross-cases 154/154, smoke PASS —
npm testgreen (7 suites). - Behaviour changes:
rg --hidden/--no-ignore/-uand root-recursivegrepare now refused (use scoped paths); a corrupt.git/configor index refuses the review instead of degrading; the child git env pins seven more keys (a repo can no longer select an editor/diff-filter/gpg program for the reviewer); restart the MCP host (policy fingerprint changed).
v1.18.5
1.18.5 — 2026-10-30
Third audit of the stacked PRs (#1–#4): 7 P0 / 14 P1 / 22 P2 / 8 P3, every P0 reproduced against real git, real hook processes and a real stdio server before counting. This release fixes all 7 P0 and all 14 P1, plus the selected P2/P3 items below.
- [P0] Repository-controlled code execution via the
ext::transport.protocol.ext.allowwas in the server's own-chardening list but not in the claude child'sGIT_CONFIG_*environment, so a repo could setremote.origin.url = ext::<command>plusprotocol.ext.allow = alwaysand the allowlisted (and prompt-suggested)git remote show originexecuted it. The child environment now neutralisesprotocol.ext.allow=neverandprotocol.file.allow=never, and the scanner refuses transport/URL keys (protocol.*.allow,url.*.insteadOf,remote.*.vcs). - [P0]
.git/config.worktreewas never scanned in the main worktree. A repo withextensions.worktreeConfig=truecould keep a livediff.<name>.textconvthere andgit log -pexecuted it. The main worktree now readsconfig.worktreetoo (linked worktrees already did). - [P0]
node_modules/was pruned from the.gitattributeswalk. Git applies attributes from every directory level, so a committednode_modules/.gitattributesactivated a repo-supplied driver while the scanner saw nothing. The scanner now enumerates every tracked/untracked.gitattributesviagit ls-files -z, coveringnode_modules/as well. - [P0] Unambiguous long-option abbreviations bypassed the deny tables.
git blame --cont(--contents),sort/wc --files0(--files0-from),git blame --ignore-revs-file/--mailmap-fileall read host files while the exact spellings were denied. Deny matching now accepts unambiguous prefixes, and the missing file-reading options are denied. - [P0]
gh pr comment --body-file/-F <host path>read a host file and POSTed it to GitHub (postToPR grants the command). The file argument is now scoped to the repository,--reposhape-checked. - Read boundaries: the PreToolUse matcher is catch-all and the hook now confines
Grep/Globpathto the repo (and applies the secret-file list), and a Bash command that references.env/id_rsa/.ssh//credentials/.npmrc/.git-credentialsetc. is refused — previously only theReadtool was protected. - fix-mode persistence:
.husky/**,.github/actions/**,.gitlab-ci.yml,Jenkinsfile,Makefile,.pre-commit-config.yaml,.devcontainer/**,.cargo/config(.toml),.circleci/**,.npmrcare refused (package.json only when the written content carriesscripts/postinstall/preinstall). find -- <path>andecho "prefix"*/echo ~"/".ssh/*(quoted-partial globs) no longer escape the in-repo path/glob checks;echoquoting is judged per character, so literal forms (echo '*',echo 'price: $5') still work.- Repo config enumeration hardening: an
[include]target that exists but is unreadable is now a refusal (was silent), more than 200 include files is refused (1200 includes used to freeze the event loop for 13 s), and the scanner rejectsprotocol.ext/file.allow,url.*.insteadOf,remote.*.vcs. - [P1]
baseRefis honoured inscope=auto(the tool default) instead of being silently dropped: a named ref now selects that branch range in both the prompt and the native command. - [P1] Synchronous failures no longer report
status:"running": the sync path settles the job before stamping it, so a cancelled/timed-out/failedwaitForResult=truecall (and every later poll) reportsfailed. - [P1]
postToPRnow requirespr(preflight refuses otherwise), andscope=prkeeps the caller's full selector (owner/repo#N, URL) instead of letting the reviewer fall back to a bare number resolved against the reviewed repo's remote. - [P1]
scripts/init.mjsships in the npm tarball again (it was excluded by the narrowedfileswhilenpm run initstill pointed at it), and it now includes gateway credentials that live only in~/.claude/settings.json. - Gate honesty: the policy fingerprint covers the hook's
PROTECTED_WRITE/SECRET_READtables (a hook-only policy change now trips the stale-policy check), the smoke test fails on aMISCONFIGUREDverdict instead of printing PASS, and the worktree fixture that used to be a vacuouscheck(..., true, true)is an explicit SKIP. - Prompt/playbook single sources:
REPORT_FORMATis extracted from the bundled SKILL.md (one contract, no second copy), fleet numbers come fromFLEET_NUMBERS(tool schema, prompt and README all render from it; SKILL.md no longer repeats numbers), andscope=autoordering is PR > baseRef > working-dir > last commit everywhere. - Suites: shell-guard 272, audit-fixes 305, p0-regression 163, validation 30, pack 43, cross-cases 154/154 (new C27: auto+baseRef, postToPR without pr, sync-failure job status), smoke PASS —
npm testgreen (7 suites). - Upgrade notes:
postToPRwithoutpris now rejected;focus/outputLanguageare stricter (from 1.18.4); the policy tables changed, so restart the MCP host;claude_review_envis no longer advertised as read-only (it can install/uninstall the skill).
v1.18.4
1.18.4 — 2026-10-29
Clears the remaining P2/P3 backlog (the triage in docs/04-test/18-triage-p2-p3-v1.18.4.md): the guard-policy holes, the runtime/protocol rough edges, prompt/playbook consistency, and the test/packaging debt. No behaviour change in the happy path; the changes below are the ones a caller can observe.
- Guard policy (security-adjacent).
git branch -u<ref>(and-u <ref>,--set-upstream) is refused — it writes.git/configfrom a read-only review (P2-11). In fix mode the two guard files (scripts/pre-tool-hook.js,src/shell-guard.js) can no longer be written, so reviewing this package can't disarm every later run (SEC-15). Review mode now refuses toReadsecret material inside the repo (.env*,id_rsa*,*.pem/*.key,credentials,.ssh/.aws/.gnupg,.netrc,.pgpass;.env.example-style templates stay readable) (SEC-23). A Bash payload whosecommandis not a string is denied instead of allowed (SEC-24), andNotebookEditnow readsnotebook_pathso fix mode can actually edit notebooks (SEC-25). - Loopback detection normalises the forms
URL.hostnameactually produces:[::1],localhost., zone ids and IPv4-mapped IPv6 — the local-gateway Bearer token can no longer be routed through an HTTP proxy by spelling the host differently (SEC-09). - Transcript recovery works on macOS again:
isAllowedTranscriptPathcompares realpaths (with the nearest existing ancestor for missing tails), so the/varvs/private/varspelling no longer makes a 429-killed subagent's recovered report silently vanish (R-7). - Process lifecycle: SIGTERM/SIGINT/SIGHUP exit with 143/130/129 instead of 0, so a host can tell a signal from a clean shutdown (R-21); a client that closes stdin now triggers the same shutdown path as
transport.close()(measured before: a running review kept billing after stdin closed);transport.oncloseruns the SDK's own cleanup first (P-12). - Cache/TTL honesty: npm-root probe failures expire after 60 s (was permanent), and
--version/--helpcaches carry a 5-minute TTL, so an in-place upgrade is picked up without restarting the host (R-8/R-9). - Robustness:
parseResultJsonparses pretty-printed results,parseUnrecognizedModelshandles nested JSON,looksLikeLaunchAckno longer discards a real report that starts with the ack sentence, the 2-hour sweep flag is set only after a successful readdir, and the deadagentResultsmap (which pinned every tool_result in memory) is gone (R-11/R-14/R-15/R-16/R-19). - Async contract: the first response is bounded by a 20 s soft preflight budget (probes that time out degrade with a warning instead of hanging the call),
resolveClaudeCliForCallwaits for the settled CLI probe,cancel:truewithout a jobId is an error, job retention is a 10-entry / 32 MiB budget (an evicted result polls asisError+status:"evicted"), and every job-shapedstructuredContentcarries{jobId, status, repoPath}(P-03/P-05/P-06/P-11/P-14/P-20). - Tool metadata: the four tools carry
title+annotations(readOnlyHintfor env/status) and register throughregisterToolwhen the SDK provides it, with a fallback for older SDKs (P-13). - Validation tightened:
focusmust be a single line (tabs/newlines/CR refused),outputLanguageis ≤20 chars with no sentence punctuation, and a PR reference whose owner/repo starts with-is refused (P2-7/P3-1/P3-3). - Prompt/playbook: one report contract (already in 1.18.3) is now also the SKILL.md text; the fleet is described as "5 briefs + 3/5 split finders" (3+5=8, 5+5=10) instead of "verbatim";
scope=autoorder is PR > branch > working-dir > last commit everywhere; CONFIRMED=85-100 / PLAUSIBLE=75-84 / REFUTED=0 is documented, so the>= 75filter and "keep CONFIRMED and PLAUSIBLE" select the same set; a stale skill that cannot be refreshed now degrades to the bundled fleet pipeline instead of running the old one silently (P2-2/P2-3/P2-8/P2-9/P2-10/P2-12/P2-13). - Packaging:
filesis narrowed to the runtime set (tarball 30 files/789 KB → 15 files/551 KB) and a newscripts/pack-test.mjs(38 checks) asserts the tarball contents and boots the packed server;scripts/lib/mcp-client.mjsreplaces the six hand-rolled stdio clients in the free suites;init.mjswrites its config atomically (temp + fsync + rename) and no longer half-writes on failure. - Suites: shell-guard 260, audit-fixes 300, p0-regression 126, validation 23, pack 38, cross-cases 150/150, smoke PASS —
npm testgreen (7 suites). - Upgrade note: the policy tables changed, so a running MCP host now refuses to start a review until it is restarted (the stale-policy self-test).
focus/outputLanguageinputs that used to pass may now be rejected.
v1.18.3
1.18.3 — 2026-10-29
Third pass: the items the previous two rounds left open, plus two findings from the maintainer's re-review (the policy fingerprint was cached for 5 minutes, and a run killed by a signal after printing a result was still reported as success).
- [P1-16] Two concurrent fix-mode reviews on the same repository could overwrite each other's working tree. Fix mode now takes a per-repository lock (
activeFixRepos, keyed by the resolved path) for the duration of the run; a second fix request is refused with an actionable message while read-only reviews stay unrestricted. The lock is released on every settle path and on every preflight refusal, so a rejected call cannot leave a repository locked. - [P1-8] A cancelled synchronous review lost the paid-for work.
waitForResult=truenever registered a job, so when the client cancelled, the SDK dropped the response and the subagent reports were unreachable. Both review and followup now create and register the job before branching: a cancelled sync call leaves a pollable job (claude_review_statuswith no jobId lists it by repository), and the synchronous return value is unchanged. - [P2-14] The stale-process fingerprint check was cached for 5 minutes. The guard self-test result carries a TTL, but the hook is spawned from disk on every call — so an upgrade without a host restart could run up to 5 minutes with two different policies while everything looked green. When
codeDrift()says the sources on disk are newer than the process, the self-test now runs withrefresh: trueand refuses immediately. - [SEC-14]
CLAUDE_REVIEW_ALLOW_UNGUARDED=1also bypassed a failed guard installation. That is exactly the case it must never bypass: the run would advertise a guard while no command is policed. The escape hatch now applies only to a guard the operator deliberately switched off (CLAUDE_REVIEW_SHELL_GUARD=0); installation and self-test failures always refuse, with instructions to fix the guard. - [P2-7] A run killed by a signal (or exiting non-zero) after printing a result was reported as success. Only the JSON event was consulted. The report is now preserved, but the result is
isError: truewithstatus: "completed_with_error", a banner naming the signal/exit code, andexitCode/signalin structuredContent (followup too). - [P2-4] Result size had no bound. A very large report could exceed what an MCP client will deliver.
claude_review_statusnow caps the text andstructuredContent.reportat 512 KiB with UTF-8-safe truncation, a marker line, and pointers to the log/partial-report files;job.resultitself is untouched, so repeated polls do not accumulate markers. - [P2-1] Two report formats coexisted in one prompt.
REPORT_FORMAT, the bundledSKILL.mdblock, the native appendix and the fallback prompt are now one contract, verbatim:## Code Review Summary/<verdict>,## Findings (N)with one### [SEVERITY] title — path:lineblock per finding + What/Why/Fix/Evidence, severitycritical | major | minor | nit,## Notes, and an explicit no-findings branch. The fleet prompt carries the block exactly once. - [P2-5]
cancel-test.mjsonly worked on Windows (tasklist). It now inspectsps -axo pid=,command=on POSIX and matches the resolved claude CLI path, so the real-CLI cancellation path can be exercised on macOS/Linux (still opt-in and billed). - [scanner] Three more command-executing git keys are refused:
pager.<subcommand>,core.askPass,sequence.editor(live values only — a dead path stays a note;pager.*booleans are inert and allowed). UNC values (//server/shareor the backslash form) are refused before any filesystem probe, so a Windows UNCcore.attributesFile/[include]no longer stalls the pre-spawn scan for ~21 s. - Suites: shell-guard 238, audit-fixes 272, p0-regression 95, validation 16, cross-cases 149/149 (C1–C26: new C23 signal-exit, C24–C26 concurrency/sync-job), smoke PASS —
npm testgreen.
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.