github kenryu42/cc-safety-net v2.4.0

latest release: v2.4.1
3 hours ago

v2.4.0 rebuilds the runtime from the ground up. The hook, CLI, API, GUI and all thirteen host integrations keep their behavior, verified command by command against v2.3.4 before the old code was removed. What changes is what you feel: every hook call is less than half as long, the package has no runtime dependencies, and secret protection now follows cd.

Highlights

  • Hook calls are 2.2× faster. A tool call on Claude Code went from 195 ms to 85 ms on the verification machine, and the p90 tail from 229 ms to 89 ms. Peak memory per call dropped from 76 MB to 56 MB and CPU time from 220 ms to 94 ms.
  • Zero runtime dependencies. zod is gone, the published manifest declares no dependencies, and package verification fails if one reappears. The Amp and OpenClaw plugin bundles shrank from 428 KB to 314 KB each.
  • Secret protection follows cd. cd ~ && cat .ssh/config and its pipe, redirection and variable forms are now denied at every level, with the operand reported as the evidence. No policy change is needed.
  • Fewer false positives. jq filters ending in .key (#112), << inside interpreter code under strict (#111), a POSIX heredoc feeding a Python script, and string literals inside inline Python, JavaScript, Ruby, Perl and PHP code (#114) are no longer denied.

Performance

Hook invocation, measured as hooks/hooks.json runs it: a fresh Node process per tool call, isolated HOME, fresh git repository as the working directory. Median of 40 interleaved rounds, Node 22 on a 4-core Linux container, bare node -e "" ≈ 40 ms. Decisions were identical on every scenario.

Scenario v2.3.4 v2.4.0 Δ
allow ls -la 195 ms 85 ms −57%
deny rm -rf / 183 ms 80 ms −56%
git status && bun test | grep x 206 ms 98 ms −53%
p90, allow path 229 ms 89 ms −61%
peak RSS 75.5 MB 55.9 MB −26%
CPU, user + system 220 ms 94 ms −57%

Time spent above bare Node, the part cc-safety-net controls, went from ~155 ms to ~45 ms.

In-process callers see the same gain. Through the public checkCommand API, a decision costs ~1.5 ms instead of ~3 ms, so the OpenCode, Pi, Amp and OpenClaw integrations handle ~670 decisions per second where they handled ~330.

Where the time went:

  • The hook loads only what a decision needs. The bin resolves the hook verb before one dynamic import of the CLI, so the hook path loads about 350 KB of JavaScript instead of about 808 KB. explain, status, doctor and the rest sit behind that import and pay for it only when invoked.
  • node:crypto left the hook's static closure. Audit ids and temp-file suffixes need uniqueness, not unpredictability, and now come from a Math.random helper with the same sixteen-hex shape. A test fails if the hook's import closure grows past 400 KB (measured 344 KB).
  • The hook ships as a CommonJS bundle behind a compile-cache loader. Skipping the ES module loader is worth 13 to 15 ms per call. dist/bin/cc-safety-net.js is now a tiny loader that enables Node's compile cache and requires dist/bin/hook.js, so the bundle's bytecode is cached after the first run under ~/.cc-safety-net/compile-cache. The first run per home pays 15 to 30 ms once; a Node without the API runs uncached.
  • Secret protection stops re-deriving constant paths. Coding-CLI roots are memoized per decision, and win32.parse is skipped on paths without a backslash. This is most of the in-process throughput gain.

Measured and rejected: a V8 startup snapshot (7 MB per Node version, only 3 ms better than the shipped bin) and running the bin under Bun. A persistent daemon is the only way below the current floor and stays out of scope.

Two costs worth knowing: the npm tarball grew from 521 kB to 532 kB because the CommonJS hook bundle carries its own copy of the gate beside the ESM chunk the other entries share, and one ~220 KB compile-cache file per Node version accumulates under ~/.cc-safety-net/compile-cache.

Changed

  • Changed secret-protection path resolution to follow the shell's directory scope: cd - returns to the previous directory, a cd inside a subshell, $( ), backticks or a process substitution ends with that shell, and a brace group or function body keeps its cd. A sh -c body is walked as shell, so a cd inside it counts. pushd/popd stay untracked. Commands that read a protected file after a cd are denied where they were previously allowed. No policy, rule or level change is required.
  • Changed standard mode to keep an interpreter string literal as a path candidate only when the surrounding code holds a filesystem, command-execution or eval marker. Strict mode still keeps every literal and scans inside literal text.
  • Improved the Config warning: line a denial carries when policy.json holds something the loader will not accept: it now names the dropped section or field, for example safety.level: not one of standard, strict, paranoid, instead of repeating schema wording. cc-safety-net doctor and cc-safety-net policy check keep their existing wording.

Fixed

  • Fixed inline jq, gojq and jaq filters ending in .key being denied as sensitive filenames (#112).
  • Fixed << inside interpreter code, such as a JavaScript template literal, blocking under strict with an unsupported-heredoc error (#111).
  • Fixed a POSIX heredoc being auto-detected as PowerShell, which denied a command whose heredoc body merely mentioned a PowerShell command.
  • Fixed interpreter code being scanned as shell text: a heredoc, pipe or -c body handed to a non-shell interpreter is now read as code, so its string literals no longer become bare path operands. A literal passed to a shell-exec call such as os.system, subprocess.* or execSync is still walked as shell and still denies (#114).
  • Fixed a sh -c body being matched by the inline-code scanner rather than walked as shell, which denied a display-only printf '%s' .env inside sh -c while allowing the same command at the top level; a body that reads a protected file, such as sh -c 'cd ~ && cat .ssh/config', now denies.
  • Fixed printf format operands without conversions being forwarded as path candidates in pipe analysis.
  • Fixed a find -exec body inside an unknown wrapper command skipping the full analysis, which allowed shapes such as custom-tool -x find . -exec dd of=/dev/sda \;.
  • Fixed non-BMP characters, including emoji, being corrupted when decoding quoted filenames in Git patch headers.
  • Fixed a git config probe inside a linked worktree timing out after 2 seconds on a cold process, which withheld a relaxation and could deny a command that should have been allowed; the cap is now 5 seconds.
  • Fixed the policy GUI answering from the repository shape it saw at startup, so a .git marker created or removed while the GUI is open now changes the command tester's answer immediately.

Removed

  • Removed the zod runtime dependency. The published package declares no dependencies and no longer ships THIRD_PARTY_LICENSES.txt.

Security

  • Improved diagnostic redaction: an assignment whose quote is never closed now has its entire remaining value redacted, instead of leaving the tail of the value in diagnostic output.

Don't miss a new cc-safety-net release

NewReleases is sending notifications on new releases.