Skip to content

Releases

v1.19.0

Choose a tag to compare

@KentQi KentQi released this 03 Nov 03:25

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) and skills/code-review/SKILL.md in the fixed discovery location. The skill moved from assets/skills/code-review/ to skills/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.md is now a portable Agent Skills skill: frontmatter gains license/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.js still extracts the report block from this file, so there is exactly one copy.
  • New plugin-test suite (offline, free): validates plugin.json (closed schema, canonical $schema, name/version/license constraints), mcp.json (closed top-level, same spec version, stdio variant rules, reserved env names, cwd forms) and every discovered SKILL.md (Agent Skills frontmatter, name↔directory, description bounds, body limits, report-contract presence). Added to npm 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 test green (8 suites).

v1.18.10

Choose a tag to compare

@KentQi KentQi released this 03 Nov 02:51

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-review was 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.mjs ships 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-*.log described Windows only; both READMEs now say "system temp dir / cc-code-review/run-*.log (Windows: %TEMP%)".
  • No functional change and no test change; npm test stays green (7 suites).

v1.18.9

Choose a tag to compare

@KentQi KentQi released this 01 Nov 05:01

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.mjs asserted codeDrift(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 though codeDrift() 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 main after 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.md was missing the round-8 report entry (20) and still declared version: 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 test green (7 suites).
  • Release line: v1.18.8 stays at its release commit (7cbbfca); v1.18.9 marks this docs/test-only release. No behaviour change.

v1.18.8

Choose a tag to compare

@KentQi KentQi released this 01 Nov 05:01
7cbbfca

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) to PROTECTED_WRITE to close R-2 — but the hook and the server always load from the package install directory, so a FOREIGN repository's src/index.js is ordinary source and editing it changes nothing. The rule therefore denied fix mode the one thing it exists for in ordinary projects (src/app.js was refused). The pipeline patterns now live in SELF_REVIEW_PIPELINE and are applied only when the reviewed tree IS this package (isSelfReview() compares the filesystem identity of the package root and CLAUDE_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 test green (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

Choose a tag to compare

@KentQi KentQi released this 01 Nov 05:02
8a2f7ea

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-from bypass. GNU find's historical long options are single-dash (-files0-from), and the new abbreviation matcher only applied to -- table entries: find -files0-from was denied while find --files0-from / find --files0 were allowed and dumped a host file's text into stderr (and the report). The deny table now lists both spellings and denyReasonIn() 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 -D takes debugopts (letters/commas) and -O a level (digits); any other token is a START PATH. find -D /etc and find -O /etc therefore 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_WRITE covered only the hook and shell-guard, while editing src/index.js (spawns the reviewer), src/claude.js (builds the child git env), src/prompt.js (renders the permission rules) or src/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/** and assets/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_name used to exit(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: engines is >=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 tilde array is gone (derived from unquoted); writeGuardSettings() reuses ensureOwnedPrivateDir() instead of a second copy; GIT_HARDENING's env/args booleans collapse to one harden flag (they could never legitimately differ — the drift that produced P0-1); looksLikeLaunchAck/launchAckBody share one ackShape(); the hook computes containment once instead of calling repoRelative() and then isInsideRepo() on the same path.
  • postToPR requires a fully-qualified PR selector. A bare number (pr: "123") is resolved by gh against the reviewed repository's own remote — and that repository is untrusted content, so its origin would decide where the summary comment is published. owner/repo#N or a PR URL pins the target to what the caller named; reading a PR by bare number stays allowed (that is the documented scope=pr behaviour).
  • 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.yml spelling was added to the globs; GIT_HARDENING and reportContractSource() 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 baseRef row 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 test green (7 suites).
  • Behaviour changes: find --files0-from / --files0 and find -D|-O <path> are refused; fix mode can no longer edit this package's own src/*.js / hook / init / lib / assets; malformed hook payloads deny; Node 20+ is required; restart the MCP host (policy fingerprint changed).

v1.18.6

Choose a tag to compare

@KentQi KentQi released this 01 Nov 05:02
664a160

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 -c args 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 -c alone never reached the claude child — exactly the class that produced the protocol.ext.allow RCE. They are now generated from a single GIT_HARDENING table, 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, repoRelative and isInsideRepo(root, token, {allowRoot}) now live in src/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 recursive grep -r/-R rooted 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.json Edit install hooks. The hook checked only Write content; an Edit/MultiEdit could add scripts.postinstall one 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/isInsideRepo without 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/config git 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 pruned node_modules/ would otherwise be unverified.
  • Lockfile registry (P2-22). All 94 resolved tarball URLs pointed at registry.npmmirror.com; they now point at registry.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 test green (7 suites).
  • Behaviour changes: rg --hidden/--no-ignore/-u and root-recursive grep are now refused (use scoped paths); a corrupt .git/config or 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

Choose a tag to compare

@KentQi KentQi released this 01 Nov 05:02
414dcfd

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.allow was in the server's own -c hardening list but not in the claude child's GIT_CONFIG_* environment, so a repo could set remote.origin.url = ext::<command> plus protocol.ext.allow = always and the allowlisted (and prompt-suggested) git remote show origin executed it. The child environment now neutralises protocol.ext.allow=never and protocol.file.allow=never, and the scanner refuses transport/URL keys (protocol.*.allow, url.*.insteadOf, remote.*.vcs).
  • [P0] .git/config.worktree was never scanned in the main worktree. A repo with extensions.worktreeConfig=true could keep a live diff.<name>.textconv there and git log -p executed it. The main worktree now reads config.worktree too (linked worktrees already did).
  • [P0] node_modules/ was pruned from the .gitattributes walk. Git applies attributes from every directory level, so a committed node_modules/.gitattributes activated a repo-supplied driver while the scanner saw nothing. The scanner now enumerates every tracked/untracked .gitattributes via git ls-files -z, covering node_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-file all 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, --repo shape-checked.
  • Read boundaries: the PreToolUse matcher is catch-all and the hook now confines Grep/Glob path to the repo (and applies the secret-file list), and a Bash command that references .env/id_rsa/.ssh//credentials/.npmrc/.git-credentials etc. is refused — previously only the Read tool was protected.
  • fix-mode persistence: .husky/**, .github/actions/**, .gitlab-ci.yml, Jenkinsfile, Makefile, .pre-commit-config.yaml, .devcontainer/**, .cargo/config(.toml), .circleci/**, .npmrc are refused (package.json only when the written content carries scripts/postinstall/preinstall).
  • find -- <path> and echo "prefix"* / echo ~"/".ssh/* (quoted-partial globs) no longer escape the in-repo path/glob checks; echo quoting 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 rejects protocol.ext/file.allow, url.*.insteadOf, remote.*.vcs.
  • [P1] baseRef is honoured in scope=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/failed waitForResult=true call (and every later poll) reports failed.
  • [P1] postToPR now requires pr (preflight refuses otherwise), and scope=pr keeps 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.mjs ships in the npm tarball again (it was excluded by the narrowed files while npm run init still 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_READ tables (a hook-only policy change now trips the stale-policy check), the smoke test fails on a MISCONFIGURED verdict instead of printing PASS, and the worktree fixture that used to be a vacuous check(..., true, true) is an explicit SKIP.
  • Prompt/playbook single sources: REPORT_FORMAT is extracted from the bundled SKILL.md (one contract, no second copy), fleet numbers come from FLEET_NUMBERS (tool schema, prompt and README all render from it; SKILL.md no longer repeats numbers), and scope=auto ordering 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 test green (7 suites).
  • Upgrade notes: postToPR without pr is now rejected; focus/outputLanguage are stricter (from 1.18.4); the policy tables changed, so restart the MCP host; claude_review_env is no longer advertised as read-only (it can install/uninstall the skill).

v1.18.4

Choose a tag to compare

@KentQi KentQi released this 01 Nov 05:02
440147b

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/config from 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 to Read secret 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 whose command is not a string is denied instead of allowed (SEC-24), and NotebookEdit now reads notebook_path so fix mode can actually edit notebooks (SEC-25).
  • Loopback detection normalises the forms URL.hostname actually 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: isAllowedTranscriptPath compares realpaths (with the nearest existing ancestor for missing tails), so the /var vs /private/var spelling 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.onclose runs the SDK's own cleanup first (P-12).
  • Cache/TTL honesty: npm-root probe failures expire after 60 s (was permanent), and --version/--help caches carry a 5-minute TTL, so an in-place upgrade is picked up without restarting the host (R-8/R-9).
  • Robustness: parseResultJson parses pretty-printed results, parseUnrecognizedModels handles nested JSON, looksLikeLaunchAck no 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 dead agentResults map (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), resolveClaudeCliForCall waits for the settled CLI probe, cancel:true without a jobId is an error, job retention is a 10-entry / 32 MiB budget (an evicted result polls as isError + status:"evicted"), and every job-shaped structuredContent carries {jobId, status, repoPath} (P-03/P-05/P-06/P-11/P-14/P-20).
  • Tool metadata: the four tools carry title + annotations (readOnlyHint for env/status) and register through registerTool when the SDK provides it, with a fallback for older SDKs (P-13).
  • Validation tightened: focus must be a single line (tabs/newlines/CR refused), outputLanguage is ≤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=auto order is PR > branch > working-dir > last commit everywhere; CONFIRMED=85-100 / PLAUSIBLE=75-84 / REFUTED=0 is documented, so the >= 75 filter 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: files is narrowed to the runtime set (tarball 30 files/789 KB → 15 files/551 KB) and a new scripts/pack-test.mjs (38 checks) asserts the tarball contents and boots the packed server; scripts/lib/mcp-client.mjs replaces the six hand-rolled stdio clients in the free suites; init.mjs writes 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 test green (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/outputLanguage inputs that used to pass may now be rejected.

v1.18.3

Choose a tag to compare

@KentQi KentQi released this 01 Nov 05:02
279cd4f

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=true never 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_status with 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 with refresh: true and refuses immediately.
  • [SEC-14] CLAUDE_REVIEW_ALLOW_UNGUARDED=1 also 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: true with status: "completed_with_error", a banner naming the signal/exit code, and exitCode/signal in structuredContent (followup too).
  • [P2-4] Result size had no bound. A very large report could exceed what an MCP client will deliver. claude_review_status now caps the text and structuredContent.report at 512 KiB with UTF-8-safe truncation, a marker line, and pointers to the log/partial-report files; job.result itself is untouched, so repeated polls do not accumulate markers.
  • [P2-1] Two report formats coexisted in one prompt. REPORT_FORMAT, the bundled SKILL.md block, the native appendix and the fallback prompt are now one contract, verbatim: ## Code Review Summary / <verdict>, ## Findings (N) with one ### [SEVERITY] title — path:line block per finding + What/Why/Fix/Evidence, severity critical | major | minor | nit, ## Notes, and an explicit no-findings branch. The fleet prompt carries the block exactly once.
  • [P2-5] cancel-test.mjs only worked on Windows (tasklist). It now inspects ps -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/share or the backslash form) are refused before any filesystem probe, so a Windows UNC core.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 test green.

v1.18.2

Choose a tag to compare

@KentQi KentQi released this 01 Nov 05:02
4fbb1d6

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.sshCommand could execute during a review. The config scanner used \bcommand\s*=, which never matches sshCommand (camelCase: h is a word char), so repoRunsGitPrograms() returned no refusals while the allowlisted — and prompt-suggested — git remote show origin ran the repo's command twice before any network I/O. The scanner now reads config through git config --file <cfg> --no-includes --list -z (normalised keys, GIT_HARDENING_ARGS + isolated gitSpawnOpts) and refuses core.sshCommand, core.fsmonitor, core.pager, core.gitProxy, credential.helper, core.attributesFile, diff.external, and every live diff.*/filter.* driver; the regex path remains as a fallback when git is unavailable. The reviewer child process also gets GIT_CONFIG_* command-scope neutralisation (7 keys) so its own git calls cannot be steered by repo config.
  • [P0] core.attributesFile was never scanned. Git has three attribute sources; only the worktree .gitattributes and .git/info/attributes were read, so a repo could point core.attributesFile at its own file and have git log -p execute 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_FILES hit inside the directory loop did not set truncated — 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 named aN.gitattributes are not attribute files and were never counted; the honest fixture is one .gitattributes per directory, and the regression uses that.)
  • [P0] echo could exfiltrate host credentials. echo was exempt from path checking and a bare $VAR was not modelled as substitution, so echo $ANTHROPIC_API_KEY, echo ~/*, echo ../* were all allowed by the real hook (the child process inherits the host environment, and SECURITY.md promises no credential exfiltration). $VAR/$1/$? are now substitution and unquoted globs/~ in echo operands are refused — quote-protection is tracked by the tokenizer, so the legitimate literal forms (echo 'price: $5', echo '*') still run, while echo $ANTHROPIC_API_KEY and echo ~/* are denied. The tool description and SKILL.md say literal text only.
  • [P0] tr file operands were unchecked (defense in depth: neither BSD nor GNU tr takes 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) rel became null and the check was skipped, so Read /etc/passwd passed — while the Write branch denied. Read now denies on the same conditions, including rel === 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] baseRef was validated by two different validators. HEAD~1/feature@2 passed index.js and were then silently dropped by prompt.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=pr without pr rendered "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 classifies permission_denials exactly like review, and reports its own allowlist gaps too.
  • [P1] The shipped SKILL.md confidence scale (0/25/50/75/100) contradicted its own >= 80 filter — 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: activeSubagentFiles was 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 the cond slot), 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 kept npm test green. It is required again; a missing suite is a failure.
  • [P1] The test suite leaked real credentials into $TMPDIR: cross-cases spawned init.mjs with the full host environment and never removed its temp tree. It now scrubs ANTHROPIC_*, 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: 0o700 only applies on creation, and on Linux /tmp is shared — a pre-created directory (or a symlink to an attacker's) received run logs and guard-settings-*.json, which claude reads at spawn time. Both paths now refuse symlinks and foreign ownership and force 0700.
  • [P1] claude_review_env could throw on a malformed ANTHROPIC_BASE_URL (new URL() unguarded) instead of reporting it. Guarded and reported as unparseable.
  • [P1] overrides never reached consumers (npm only honours the root project's overrides), so the @hono/node-server pin 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 any claude-* 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 through err() instead of preflightFail(), 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): tr has no FILE operand in BSD/GNU coreutils (the guard change is defense in depth, not an arbitrary-read fix); the 4100-file MAX_ATTR_FILES repro was invalid; echo /etc/passwd prints a literal, not file contents — the real leak was wildcard/$VAR expansion.
  • Suites: shell-guard 238, audit-fixes 272, p0-regression 95, validation 16, cross-cases 139/139 (C1–C22), smoke PASS — npm test green.