Conversation
…e directory resolution, and refine secret redaction patterns
…with legacy flag support
… descriptive denial reasons across command checks and git rules.
…ion error messages
…d corresponding test coverage
…ss within -exec clauses
…nd system info integration
…e installation instructions
…support for multiple coding agents
…lugins and refactor internal command execution
… uninstallation workflows
… lolcat rendering per line
…ent for doctor command
… improved focus styling
…put instead of config file parsing
…ss install and doctor logic
… usage-based messaging
Phase 5e of the coding CLI secret path plan, plus the Phase 6b limitation notes. Each product gets its mixed settings and MCP config files in the config tier: - Antigravity: ~/.gemini/config/mcp_config.json - Codex: ~/.codex/<name>.config.toml named profiles - Gemini CLI: <project>/.gemini/settings.json and the three system roots - Copilot CLI: new secret.cli.copilot-cli.config for mcp-config.json - Pi: new secret.cli.pi.config for models.json - Kimi Code: <project>/.kimi-code/mcp.json - OpenCode: <project>/opencode.json and .jsonc docs/secret-protection-known-limitations.md records three boundaries that readers assume are already written down: operating-system keyrings, Windows %VAR% path forms, and the deliberate .envrc exception.
…sion Both changes reduce protection, so they land together and apart from every addition in the plan. A bisect can thus tell an addition from a removal. The extension rules had a routine false positive: a project with db/schema.sql, a migration directory, or a local tmp/dev.sqlite blocked ordinary work on every one of those files. The true positive is speculative by comparison, and the correct answer for a real credential database is a named path, as the OpenCode database rule shows. The rare extensions stay: .pcap, .rdp, .gnucash, .mdf, and .sdf cost near zero friction in a repository. The .git exclusion had a false negative: .git/hooks/deploy_key_rsa is a real place for a deploy key, and the exclusion allowed it. The .git tree is small and an agent rarely reads it in bulk, so the false-positive cost is near zero. node_modules, __pycache__, vendor/bundle, and vendor/cache keep their exclusions, because those trees hold thousands of test-fixture .pem files. BREAKING CHANGE: two rule ids are gone, secret.ext.sqlite and secret.ext-pattern.sql. A config that names either id no longer matches a rule.
Rename the "Coding CLI" secret category to "Coding CLI credential", so the Policy tab names the two tiers as "Coding CLI credential" and "Coding CLI config". The GUI renders rule.category directly, so the rename happens in the rule metadata. Add a "Version" panel to the Settings tab, between "Policy JSON" and "Danger zone". The value comes from getPackageVersion() through the /api/policy response, which the Settings tab already reads. This adds no endpoint and no request. The integrations payload is not used, because it runs a version probe for every coding CLI. Make the brand logo a link to #overview. The GUI already routes on location.hash and listens for hashchange, so the anchor is sufficient and needs no script. The link carries no data-nav attribute, because that attribute marks the one sidenav link that gets aria-current="page".
A field deny: committing this repository's own work with `git commit -q -F - <<'EOF'` denied on secret.basename.credentials, because the message body held the word "credentials" in prose. The word never named a file. Two allowlists had drifted apart. analyze-command.ts learned message sinks in 2e0fcb0 and skips a raw-text scan of their bodies, but the data sink test in semantic-facts.ts, which masks a body before path extraction, still held cat and tee alone. Every other consumer fell through, and any word in the body became a path candidate. Add git commit, gh pr create and gh issue create here as well. All three store or publish the body; none resolves a word in it as a path. All three were confirmed denying beforehand on the same one-line prose. git apply is deliberately not added, although the raw-text list holds it. The two lists ask different questions. A patch body is not a program, so it is inert for a raw-text scan, but it does name the files the patch writes, so it must stay visible to path extraction. An existing case already pins that, and it still passes.
The mixed settings and MCP config files carry credentials inline, but an agent edits them as routine work, so the config tier blocks too much for some workflows. Every rule in the "Coding CLI config" category now ships off, and the user opts in per rule. The old model had no default-off state: an override could only be "off", and a rule was active unless one existed. Four changes make the default real. - SECRET_DEFAULT_OFF_RULE_ID_SET derives the 10 ids from the category, so no rule stores a redundant flag. - resolveSecretDisabledRules keeps a default-off rule disabled until an explicit "on" override opts into it. - The secret override schema accepts "on" as well as "off", which the opt-in needs. The destructive override schema already had both. - The GUI switch state, the tier counts, and the group switch read the default instead of the presence of an override. Reset needs no new code. It writes DEFAULT_GUI_POLICY, whose overrides are empty, and empty overrides now mean the config tier is off. A test pins this. A rule that matches its default keeps no override, so the saved file stays small and a later default change still reaches the user. This mirrors the destructive tier switches. Three existing tests pinned the old defaults and the old "must be off" message. The specified behavior changed, so they change with the code.
There was a problem hiding this comment.
Important
The SecretOverridesSchema schema update accepts "on" for secret overrides, but normalizeGuiPolicy and repairOffOverrides still only preserve "off" values — a user's opt-in override for a default-off Coding CLI config rule is silently dropped during GUI read/write and policy repair.
Reviewed changes — 12 commits since 0b0e268 restructure secret protection tiers, rewrite doctor hook detection to use filesystem state files, extend heredoc message-sink handling, improve CLI help, and redesign the CI/CD pipeline.
- Split coding CLI secret rules into credential and config tiers (
src/core/secret-protection-rules.ts), with config-tier rules defaulting to off viaSECRET_DEFAULT_OFF_RULE_ID_SET. The schema now accepts per-rule"on"overrides (SecretOverridesSchemaacceptingz.enum(['on', 'off'])), andresolveSecretDisabledRulescorrectly re-enables an opted-in rule. - Removed
.sql/.sqliteextension rules and the.gitpath-segment exclusion from broad signature skip logic. - Extended heredoc message-sink consumer detection so
git commit,gh pr create, andgh issue createheredocs are treated as inert data — both in semantic facts (isMessageSinkConsumer) and in the analyzer (getHeredocReason). - Rewrote doctor integration detection (
src/bin/doctor/hooks.ts,src/bin/doctor/system-info.ts) to use filesystem state files instead of CLI output parsing across Claude Code, Copilot CLI, Gemini CLI, and Pi. Removed the Pi probe runner entirely. - Added
helpTargetto integration catalog entries for better install/uninstall help text, and updated the hook command usage to show an integration flag placeholder rather than the old shell-quote literal. - Updated
parseEnvLevelto treat emptyCC_SAFETY_NET_LEVELas absent and to log invalid-value diagnostics unconditionally. - Redesigned the CI pipeline with tag-bound publishing, pinned action SHAs, packed-runtime matrix testing, and tarball/checksum verification.
ℹ️ Claude Code doctor detection reports "disabled" when settings.json is absent
At src/bin/doctor/hooks.ts:129-141, when settings.json is missing but installed_plugins.json confirms the plugin is installed, the detection reports disabled with "installed but not enabled in Claude Code." The old CLI-based detection resolved this through the plugin's own status output. If Claude Code defaults to "enabled" when the key is absent (like Copilot CLI at line 813), this is a misleading false negative.
Note: 1 inline comment(s) dropped because they did not anchor to lines inside the PR diff:
src/core/policy.ts:421-427(RIGHT) — file has no textual diff (binary, pure rename, or mode change)
DeepSeek Pro (free via Pullfrog for OSS) (GPT Sol not used — the program covers this model; add its provider key to run your pick) | 𝕏
| status: 'disabled', | ||
| method: 'plugin config', | ||
| configPath: settingsPath, | ||
| errors: [`${CLAUDE_SAFETY_NET_PLUGIN_ID} is installed but not enabled in Claude Code`], |
There was a problem hiding this comment.
When settings.json is absent but installed_plugins.json confirms the plugin is installed, this reports disabled with "installed but not enabled in Claude Code." If Claude Code defaults to enabled when the key is absent (like Copilot CLI), the status and message are misleading. Consider treating absent settings as configured or giving a distinct message.
The GUI writes an 'on' override when a user turns on a coding CLI config rule, but normalizeGuiPolicy and repairOffOverrides kept only 'off'. The opt-in was dropped on save, on read, and on policy repair, so the rule went back off. Keep both values, and merge the two repair helpers, which differed only in the rule id set. The GUI explain preview now resolves disabled rules with resolveSecretDisabledRules, because keys of overrides no longer identify the disabled rules.
There was a problem hiding this comment.
ℹ️ The prior
[!IMPORTANT]finding is fixed —normalizeGuiPolicyandrepairRuleOverridesnow correctly preserve"on"secret overrides. No new issues from this commit. Approval blocked by 2 unresolved Pullfrog threads from earlier reviews (both pre-existing, neither touched by this commit).
Reviewed changes — 1 commit since f68ec33 fixes the [!IMPORTANT] finding from the prior Pullfrog review.
- Kept
"on"secret overrides through GUI paths —normalizeGuiPolicynow preserves both"on"and"off"secret override values.repairOffOverridesandrepairDestructiveCommandOverrideswere merged intorepairRuleOverrides, which preserves both directions.resolveSecretDisabledRulesis exported and used by the GUI explain command to correctly identify disabled rules instead of treating all overrides as disabled.
DeepSeek Pro (free via Pullfrog for OSS) (GPT Sol not used — the program covers this model; add its provider key to run your pick) | 𝕏
The invalid CC_SAFETY_NET_LEVEL case called getCCSafetyNetEnvModes without the console.error spy its sibling reporting block already uses, and captureRuleCommand replaced only console.log while the rule CLI prints parse errors through console.error. Both leaked into the runner output. The rule helper now captures stderr into the same buffer, so tests can also assert on error text.
…ntegration The v2 rename means every v1 user must come back through the installer, but there was no update path at all: the selector blocked configured targets as "already installed" and the native closures no-opped when a plugin was present. `cc-safety-net update` detects every installed integration — including installed-but-disabled ones and legacy-only v1 plugins — and runs each through its install operation, which now updates in place: Claude Code, Codex, Copilot, and Gemini run their CLIs' real update commands and drop legacy plugin ids, while OpenCode and Pi re-resolve latest from npm. Detection stays user-scope and non-mutating. Copilot presence comes from its plugin checkouts on disk, because its detection status also reports 'disabled' for a bare disableAllHooks kill-switch with nothing installed, and update must never install something new. Native targets whose CLI is missing are skipped with a notice; config-file targets update without their editor binary. Cursor, Kimi Code, and Antigravity hooks run `npx -y cc-safety-net`, which pins whatever version the npx cache holds, so their installs now clear the matching cache entries. The cache root honors npm_config_cache and the Windows npm-cache default, falling back with || because an empty env value would make the recursive-delete root cwd-relative. The README quick start uses cc-safety-net@latest for the same staleness reason: a bare spec can re-run a cached v1 binary that has no update command. The install selector keeps installed rows unselectable; a new `u` key runs the same routine as the update command, which also catches legacy-only installations that render as fresh rows. The update flow threads an `updating` flag through the install operations so output reads "Updated" and "up to date"; plain install and uninstall messages are unchanged, and explicit flags like `install --codex` still update in place for scripting. Validated with `bun run check` (4152 tests, coverage gate green) and a clean structured Codex review.
There was a problem hiding this comment.
ℹ️ No new issues from these commits. Approval blocked by 2 unresolved Pullfrog threads from earlier reviews (both pre-existing, neither touched by this commit).
Reviewed changes — commits since 6b4d4b5 add the update command and npx cache management.
- Added
updatecommand —cc-safety-net updatedetects all installed integrations (including legacy-only Claude Code, Codex, and Copilot plugins) and re-runs their install logic with anupdatingflag. Native integrations use their own update subcommands (plugin marketplace update+plugin update,plugin marketplace upgrade,extensions update) instead of fresh-install commands. Theukey in interactive install triggers the update flow. - Introduced
NativeInstallPlantype with per-integrationupdateflag so each native install command closure decides which commands to run based on whether the plugin already exists. - Centralized Copilot plugin directory constants —
COPILOT_PLUGIN_DIRandCOPILOT_LEGACY_PLUGIN_DIRmoved from local constants indoctor/hooks.tsto exported values insrc/integrations/copilot-cli.ts, shared between doctor and install. - Added npx cache clearing —
clearNpxSafetyNetCache()insrc/bin/hook/install/npx-cache.tsremoves stale npx cache entries forcc-safety-neton config-target installs, ensuring the latest binary is fetched. Correctly handles npm cache location across env vars, platform defaults, and empty values. - Added
detectInstallHookStateshared detection function that probes Codex and Copilot CLI availability in parallel, used by both the interactive install selector and the update command. - Test coverage — 12 end-to-end update tests (
tests/bin/hooks/update.test.ts), 5 npx cache tests (tests/bin/hooks/npx-cache.test.ts), updated install and TUI selection tests for theupdatesentinel.
DeepSeek Pro (free via Pullfrog for OSS) (GPT Sol not used — the program covers this model; add its provider key to run your pick) | 𝕏
… false green Three coupled improvements to the rule surface the cc-safety-net skill drives, all found while wiring the skill for the v2 release. Update reminder: the skill boots with `npx -y cc-safety-net rule doc`, and a bare npx spec pins whatever version the npx cache holds, so agents keep driving stale binaries after a release. After printing the doc, the command now polls the npm registry at most once per 24 hours (cached in ~/.cc-safety-net/update-check.json) and, when a newer version exists, prints one UPDATE_AVAILABLE line to stderr telling the agent to offer `npx -y cc-safety-net@latest update` once and continue either way. A given version re-surfaces at most weekly, failures are silent, CC_SAFETY_NET_NO_UPDATE_CHECK disables the check, and the test suite sets it so no test reaches the registry. A malformed cache counts as empty and non-finite or future timestamps (JSON's 1e999 parses to Infinity) are treated as unset; either would otherwise disable the reminder forever. Bare `rule test` tested nothing: it resolved zero sources and printed "Rulebook tests passed", a false green light this project's own skill validation steps relied on. It now loads the selected scope's rule.json (project by default, --global for user, matching rule sync) and runs every configured source's fixtures. Zero configured sources - including a missing rule.json - prints "No rulebooks configured; nothing to test." instead of success text; an invalid config fails with its diagnostics. `rule test <source>` is unchanged. A full audit of the rule doc against the implementation corrected six false claims: the priority table said project scope wins, but user scope is evaluated first and a user-claimed rulebook name shadows the project one; project overrides on user rules are ignored with a diagnostic, not "fail closed"; the degraded-state paragraph now distinguishes per-source from per-scope inactivation and says built-in protections stay active; subcommand matching accounts for git and docker option values and `--`; description and author are not type-checked. Added the override/rule `intent` enum, `$schema`, case-insensitive rule-name uniqueness, the GitHub name-match requirement, and CC_SAFETY_NET_HOME. Resource limits and migration artifacts stay undocumented on purpose: they are beyond realistic agent output. The skill and the shared OpenCode/Pi template mirror every doc change; a test pins the mirroring. Validated with `bun run check` (4168 tests, coverage gate green) and clean structured Codex reviews of each piece. BREAKING CHANGE: bare `rule test` now tests every rulebook configured in its scope instead of silently testing nothing.
Replace the inline shield data URI with the mesh SVG asset and ignore SVG assets in jscpd so geometry repetition does not fail checks.
Why: - The README duplicated ~200 lines the docs site now owns (per-agent installs, presets, audit, recovery, limitations), still framed the product as destructive-command blocking only, and never told v1 users how to upgrade. What: - Route detail to ccsafetynet.com/docs: the ten per-agent install sections collapse into Quick start plus links, the two limitation sections compress into one deferring to SECURITY.md and Known Limitations, and the docs table lists all 24 pages including the new Secret Protection reference. Per-agent badges point at docs installation anchors. The new-page links stay dead until the docs v2 branch deploys; this is accepted pre-merge. - The tagline expands the name (Coding CLI Safety Net) and, with the npm package description, names both pillars: destructive commands and secret file access. A tip in "What's new in v2.0.0" and the Upgrading section lead with `npx -y cc-safety-net@latest update` as the v1-to-v2 path. - Remove the package-entrypoints section: the toolchain facts live in CONTRIBUTING.md, and the README deliberately no longer documents the programmatic API contract. Validation: - Not run: only the README and the package description changed; no repository check covers prose. The docs-side link targets pass mint validate and broken-links in the docs repo.
Why:
- At widths of 640px or less, .tier-counts gets a full-width flex
basis so it wraps below the label, but the destructive tiers and
secret groups nest the label and counts inside a .tier-collapse
button that never wrapped. The full-width counts squeezed the label
below its text width and the two texts painted over each other
("Base9ame", "Inherits5 off").
What:
- Apply the mobile flex-wrap to .tier-collapse alongside
.rule-tier-head so the counts drop to their own line inside the
nested button too.
- Ignore the .playwright-cli directory the Playwright CLI writes
during local verification.
Validation:
- bun run check (4155 tests pass, coverage gate green).
- Served the GUI and screenshotted the Policy tab at a 390px
Playwright viewport: Strict tier, Paranoid tier, and the secret
groups render label, counts, and switch without overlap.
… binary Why: - The e2e suite hand-crafts each agent's hook payloads, so a change in Claude Code's real hook protocol or execution loop would go unseen. What: - tests/e2e-live/claude-code.test.ts spawns `claude -p` (haiku, --allowedTools Bash) in an isolated HOME with the built CLI registered as the PreToolUse hook, and asserts on hook artifacts only: an allowed command executes and audits allow; `git reset --hard` is denied, the uncommitted sentinel survives, and the deny is audited. - Local-only because runs spend real usage: tests skip unless CC_SAFETY_NET_E2E_LIVE=1, the claude binary, and CLAUDE_CODE_OAUTH_TOKEN are all present; `bun run test:e2e:live` opts in and CI never sets the flag. The harness drops ANTHROPIC_API_KEY so the agent authenticates with the subscription token. Failures print the agent transcript and audit entries because the agent is nondeterministic. Validation: - bun run test:e2e:live passed two consecutive runs (18-24 s each). - bun test tests/e2e-live without the flag: 2 skip, 0 fail. - bun run check: 4155 pass, coverage gate green.
Why: - The live suite proved the shell-command pillar against the real Claude Code binary but left the secret-file pillar simulated only. What: - Plant a fake .env with a canary value, tell the agent to read it with the Read tool and reply with the exact content, then assert the deny audit entry (secret.basename.env) and that the canary never appears in the reply. Demanding the content back makes the transcript check a real leak detector: a hook failure would surface the canary. - Allow the Read tool in the harness so the hook, not Claude Code's own permission gate, is the deciding barrier. Validation: - bun run test:e2e:live: 3 pass (27 s). - bun run check: 4174 pass, coverage gate green.
Why: - The live suite proved the hook against Claude Code only; Codex ships the same plugin hook but no test ran it against the real binary. What: - Rename the live file to protection.test.ts and parameterize the three scenarios (allow, git reset --hard, .env canary) over an agents table, mirroring the simulated suite's adapter pattern. - Codex runs `codex exec -m gpt-5.6-terra` at low reasoning effort with --dangerously-bypass-approvals-and-sandbox so the hook is the only gate, and --dangerously-bypass-hook-trust so the isolated CODEX_HOME needs no interactive /hooks trust step. Setup writes hooks.json there and copies ~/.codex/auth.json, so runs bill the ChatGPT subscription; OPENAI_API_KEY is dropped alongside ANTHROPIC_API_KEY. Codex skips independently when its binary or auth.json is absent. - The runs also confirm transcript-path agent detection against the real binary: audit entries carry agent "codex", and Codex's shell-based .env read hits the same secret.basename.env rule as Claude Code's Read tool. Validation: - bun run test:e2e:live: 6 pass on two consecutive runs (79 s, 97 s). - bun test tests/e2e-live without the flag: 6 skip, 0 fail. - bun run check: 4177 pass, coverage gate green.
Why: - The single-token raw-text scan blocked cleanly parsed quoted-literal assignments like W='rm -rf ~' even when the variable was only ever used as quoted argument data (real deny hit while probing the analyzer through its own explain CLI). The heuristic predates the IR parser: one token with spaces used to imply a failed parse, but the parser now produces such tokens from well-understood quoted words. What: - When the dangerous text sits in a cleanly parsed quoted-literal assignment, suppress the assignment-time block only if every other reference to that variable is a quoted expansion in argument position; a quoted expansion stays one argv word and cannot split into a command plus flags. Unquoted references, command-position references, references inside substitutions, and expanding heredoc bodies keep the block, and strict mode never defers. - Chains that hand the value to a shell (eval "$W", bash -c "$W", echo "$W" | sh) stay blocked by the existing shell-execution-source rule, so no executor list is duplicated. - The deferral applies in explain-legacy mode too: the guard engine never sets that flag, and explain must report the guard's decision. Validation: - bun test tests/core/analyze/deferred-assignment.test.ts: 13 pass (7 failed red before the fix). - bun run check: 4181 pass, coverage gate green. - The originally denied probe command now returns allowed via the explain CLI; the risky chains above still return blocked.
There was a problem hiding this comment.
ℹ️ No new issues from these commits. Approval remains blocked by 2 unresolved Pullfrog threads from earlier reviews (the chunk-import regex in
scripts/verify-build.tsand the vacuous residual-risk fixture harness assertion), neither of which was touched by these commits.
Reviewed changes — 9 commits since f78d172 defer one class of assignment-time false positives, close two protection gaps, add an update-availability notice, and fix a silent rule test false green.
- Deferred quoted-assignment dangerous-text blocks so
W='rm -rf ~'is no longer flagged at assignment time when the variable is only used as quoted data elsewhere in the program.isDataOnlyQuotedAssignmentinsrc/core/analyze/deferred-assignment.tswalks the full program tree and verifies that every reference to the name stays inside double quotes in an argument position — unquoted references, command-position references, expansions inside$(…), and unquoted heredoc bodies all keep the assignment-time block. Strict mode always blocks. Downstream dangers (eval "$W",bash -c "$W", pipe into a shell) are blocked by their own rules independently, so the deferral cannot open a bypass. - Removed
git.executable-configdestructive command rule (src/core/destructive-command-rules.ts) — the label, metadata, and rule ID are gone from both the rule ID list and the metadata array. The destructive command rule count drops from 56 to 55 (51 rules with no activation capability). - Extended git metadata protection to
unknownroutes (src/core/git-metadata-protection.ts) —findGitMetadataMutationTargetInSemanticFactsnow also inspects path targets whenroute.kind === 'unknown', alongsidepatchandpath. - Added signature value redaction (
src/core/sanitize.ts) —x-amz-signature,x-goog-signature,sig, andsignaturevalues in audit log entries are now redacted, both as bare assignments and inside URL query strings. Tested across double-quoted, single-quoted, and unquoted forms with;,|, and?delimiters. - Shows update-availability on
rule doc(src/bin/rule/update-notice.ts, wired intosrc/bin/rule/index.ts) — a daily-throttled version check writes a JSON cache at~/.cc-safety-net/update-check.json. The notice re-fires weekly if unaddressed. The builtin command template (src/builtin-commands/templates/cc-safety-net.ts) instructs agents to mention the update once and continue. Opt-out viaCC_SAFETY_NET_NO_UPDATE_CHECK. - Fixed
rule testfalse green (src/bin/rule/index.ts) — invoked without arguments,rule testwas passing an empty array totestRulebookSources, which silently returned success. Now it reads the actualrule.jsonconfig and passes the configured sources, failing with clear errors when the config is invalid and printing a message when no rulebooks are configured. - Slimmed README — per-agent installation detail, supported-platforms section, policy-file protection limitations, PowerShell path limitations, prerequisites, and package entrypoint notes moved to the docs site. Quick-start and v2 feature bullets trimmed. Agent badge links now point to
ccsafetynet.com/docs/installationanchors. - Added SVG favicon for the policy GUI (
src/bin/gui/favicon.svg, inlined viadata:image/svg+xmlinrenderPolicyGuiHtml). Mobile CSS extended so nested.tier-collapseheads also wrap on narrow viewports. - Added opt-in live E2E test suite (
tests/e2e-live/protection.test.ts) — gated behindCC_SAFETY_NET_E2E_LIVE=1and each agent's auth, the suite builds a fresh runtime, sets up a temporary workspace with a hook config, and drives the real Claude Code and Codex binaries through three assertions: an allowedgit status && touch allow-ran, a blockedgit reset --hardretaining uncommitted changes, and a blocked.envfile read (canary content must not appear in the transcript).
DeepSeek Pro (free via Pullfrog for OSS) (GPT Sol not used — the program covers this model; add its provider key to run your pick) | 𝕏
Why: - The shared vocabulary was split across src/types.ts, src/domain/, and config/schema.ts with no rule deciding placement; core/policy.ts held three near-duplicate normalizers over two shapes of the same data; freezePolicy froze field-by-field so a new policy field would ship mutable by omission; and custom-rule provenance lived in a WeakMap side-channel because the frozen snapshot had no slot for it. What: - Dissolve src/types.ts: analysis/audit/explain vocabulary moves into src/domain, analyzer constants into core/analyze/constants.ts, and each agent hook payload shape into its adapter under src/bin/hook. Consumers import the real homes; no compatibility barrel remains. - Collapse policy normalization to two functions with distinct roles: normalizeGuiPolicy (untrusted JSON to the canonical file shape, with per-field salvage) and normalizePolicyConfig (file shape to runtime policy). The valid-file path drops its duplicate schema.parse; a policy omitting safety.level now normalizes to 'standard', which is behavior-identical because env resolution already applied it. - Replace freezePolicy with a generic deepFreeze over structuredClone, and move rule provenance into snapshot.ruleMetadata as a plain frozen record - a Map would break the snapshot's JSON-round-trip invariant - deleting config/policy-metadata.ts. Validation: - bun run check: 4179 pass, coverage 93.3% lines / 95.0% functions. - bun run verify:build, verify:package, verify:repository-plugin all pass; the packed tarball still exposes exactly CCSafetyNetPlugin.
Why: - The behavioral contract had deny cases with no nearest-allowed neighbor in nine analyzer families, and five families (parallel, heredoc, device, awk, deferred assignment) were absent entirely. The contract is the acceptance gate for reworking the analyzer, so each family needs its allow/deny boundary recorded, not just its denials. What: - Add 30 cases forming boundary pairs across git push/stash/clean, find, xargs, parallel, interpreter one-liners, eval, pipe-to-shell, heredoc quoting, dd device writes, awk system(), sudo unwrapping, deferred assignment execution, paranoid-rm with a trusted TMPDIR target, and PowerShell Remove-Item target shapes. - Each expectation was verified against the current analyzer before being recorded; the cases document behavior, they do not change it. Validation: - bun test tests/core/analyze/behavioral-contract.test.ts: 52 pass. - bun run check: green at the branch tip.
Why: - A scope-discipline audit found checks that stop no demonstrated failure: the residual-risk validator and fixture harness were built ahead of their first automated registry entry (all ten families are legacy, so the harness generated zero tests); the ast-grep setup enforced one rule against a mistake src has never contained; the ci-workflow test asserted YAML substrings its own author edits; and generate-changelog.ts was dead but invisible to knip because scripts/ was outside its project scope. What: - Delete validate-residual-risk.ts, the fixtures and validation tests, the ast-grep directory with sgconfig.yml and its trustedDependencies postinstall grant, ci-workflow.test.ts, and generate-changelog.ts. docs/residual-risk-registry.json stays as documentation; check and check:ci drop the sg:scan and verify:residual-risk steps. - Widen knip to scripts/ with workflow-invoked scripts declared as explicit production entries. The github-actions plugin is disabled: it re-claims those same files as dev-only entries, which overrides the production markers and falsely flags their imports as unused. - Unexport three script exports nothing consumes and tag seven test-only ones @internal, per the knip --production convention. Validation: - bun run check: green (lint, typecheck, knip, jscpd, tests, coverage).
Why: - Three test files reached the developer's real ~/.cc-safety-net: the explain rm-in-home test used the real HOME as cwd, so it failed standalone (the preload points the policy path at a temp dir, so the policy-protection guard correctly stays quiet) and passed in full runs only because status/statusline tests delete CC_SAFETY_NET_HOME without restoring it. That same deletion made ten statusline tests read the real policy file - customizing it broke them all. What: - Add hermeticSafetyNetHome to tests/helpers.ts: a per-file empty temp home that restores the original CC_SAFETY_NET_HOME after the file finishes, closing the cross-file leak. - The explain test now builds its own home with the policy directory under the cwd; assertions are unchanged. status and cli-statusline point their env reset at the hermetic home instead of deleting the variable, so spawned CLIs inherit a deterministic default policy. Validation: - The three files pass standalone: 172 tests, 0 fail, with the real policy file customized. - bun run check: 4179 pass, coverage gate green.
Why: - The search-tool whitelists referenced local planning files, putting their names in the repository for no benefit: this is a single-maintainer project, so a tracked ignore file shared nothing. What: - The whitelists now live in an untracked .rgignore, itself listed in .git/info/exclude alongside the files it re-includes, so the whole arrangement is invisible to git while ripgrep-based search still indexes the whitelisted files.
Why: - Two tools plus a chmod setup script did what one can: husky only wired hook paths and needed lint-staged for staged-file linting, while lefthook does both natively from a single lefthook.yml. What: - lefthook.yml reproduces the old hooks: pre-commit runs knip, biome --write on staged files (stage_fixed re-stages fixes), then build with dist/ auto-staged, piped so a failure stops the chain; pre-push still runs bun run check. - prepare is now just "lefthook install"; the setup-hooks chmod step is gone because lefthook manages hook file permissions itself. - knip.ts drops the lint-staged ignore entry; knip's lefthook plugin detects the new dependency without any ignore. Validation: - lefthook run pre-commit with staged files: knip, biome, build all pass in order. - bun run check: 4157 pass, 0 fail, coverage 93.33% verified.
Why: - lefthook v2 runs hook jobs in a pty without a window size, so the test process saw a TTY with columns 0. The width fallback used ??, which only catches undefined, so the render width became 0: each fact row lost its last character behind an ellipsis and diagnostics wrapped one word per line, failing the TTY status test on pre-push. What: - The status width falls back with || so a 0-column TTY also renders at 80; the 100-column cap is unchanged. - renderOnTTY pins process.stdout.columns (default 80) so the suite no longer depends on the terminal that happens to run it; a new test renders at 0 columns and asserts untruncated rows. Validation: - The new test reproduced the exact pre-push truncation before the fix and passes after it. - bun run check: 4180 pass, 0 fail, coverage 93.33% verified.
There was a problem hiding this comment.
ℹ️ No new issues from these commits. Approval remains blocked by 2 unresolved Pullfrog threads from earlier reviews (the chunk-import regex in
scripts/verify-build.tsand the Claude Code doctor detection insrc/bin/doctor/hooks.ts), neither of which was touched by these commits. The residual-risk fixture harness concern from the first review body is addressed by its removal.
Reviewed changes — 8 commits since 585410a relocate the type vocabulary, drop ceremony tooling, add per-family boundary test coverage, fix a terminal-width edge case, and pin CLI-surface tests to hermetic homes.
- Relocated type vocabulary —
src/types.tsdeleted; types distributed to their domain homes (domain/analysis.ts,domain/audit.ts,domain/explain.ts,domain/policy.ts). Constants (SHELL_WRAPPERS,MAX_RECURSION_DEPTH,COMMAND_PATTERN, etc.) moved tosrc/core/analyze/constants.ts. Hook input/output interfaces inlined into each hook file.ValidationResultinlined intosrc/core/config.ts. - Embedded rule metadata into
PolicySnapshot—src/config/policy-metadata.tsremoved;ruleMetadatais now an explicitReadonly<Record<string, CustomRuleMetadata>>field populated at snapshot creation.deepFreezereplaces the field-by-fieldfreezePolicy, preventing silently-mutable new fields. - Unified normalization path —
normalizeGuiPolicyreplacesrepairPolicyConfigas the single normalizer from untrusted JSON to the canonical policy-file shape.readPolicyConfignow normalizes through the same path for both valid and salvageable input, replacing the priorgetUserPolicySchema().parse()call that could throw. - Removed ceremony tooling — ast-grep (
@ast-grep/cli,sgconfig.yml, rules/snapshots), residual-risk validation (scripts/validate-residual-risk.ts,verify:residual-risk), husky, lint-staged, changelog generation (scripts/generate-changelog.ts), and CI workflow tests (tests/scripts/ci-workflow.test.ts) all removed. Replaced husky/lint-staged withlefthook(lefthook.yml,preparescript). Knip config updated to treat build scripts as entries with thegithub-actionsplugin disabled. - Added behavioral contract boundary tests —
tests/core/analyze/behavioral-contract-cases.tsgains 35 new cases covering force-push, stash,find -exec,xargsshell child,parallel rm, interpreter one-liners, eval, piped remote script, heredocs, device writes, awk, sudo, deferred assignments, paranoid rm, and PowerShell removal. Every case asserts specificruleId,intent,reasonIncludes, andsegment. - Fixed
statusterminal-width edge case —??changed to||so zero-column PTYs (lefthook jobs) fall back to width 80 instead of 0. - Pinned CLI tests to hermetic homes — new
hermeticSafetyNetHome()helper isolatesCC_SAFETY_NET_HOMEper test file; statusline, status, and explain tests no longer read the developer's real~/.cc-safety-net.
DeepSeek Pro (free via Pullfrog for OSS) (GPT Sol not used — the program covers this model; add its provider key to run your pick) | 𝕏

No description provided.