Skip to content

feat: v2 release - #68

Open
kenryu42 wants to merge 287 commits into
mainfrom
feat/v2
Open

feat: v2 release#68
kenryu42 wants to merge 287 commits into
mainfrom
feat/v2

Conversation

@kenryu42

Copy link
Copy Markdown
Owner

No description provided.

kenryu42 added 30 commits July 3, 2026 17:09
…e directory resolution, and refine secret redaction patterns
… descriptive denial reasons across command checks and git rules.
…lugins and refactor internal command execution
kenryu42 added 5 commits July 31, 2026 12:41
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.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 via SECRET_DEFAULT_OFF_RULE_ID_SET. The schema now accepts per-rule "on" overrides (SecretOverridesSchema accepting z.enum(['on', 'off'])), and resolveSecretDisabledRules correctly re-enables an opted-in rule.
  • Removed .sql/.sqlite extension rules and the .git path-segment exclusion from broad signature skip logic.
  • Extended heredoc message-sink consumer detection so git commit, gh pr create, and gh issue create heredocs 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 helpTarget to 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 parseEnvLevel to treat empty CC_SAFETY_NET_LEVEL as 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)

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) (GPT Sol not used — the program covers this model; add its provider key to run your pick) | 𝕏

Comment thread src/bin/doctor/hooks.ts
status: 'disabled',
method: 'plugin config',
configPath: settingsPath,
errors: [`${CLAUDE_SAFETY_NET_PLUGIN_ID} is installed but not enabled in Claude Code`],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ The prior [!IMPORTANT] finding is fixed — normalizeGuiPolicy and repairRuleOverrides now 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 pathsnormalizeGuiPolicy now preserves both "on" and "off" secret override values. repairOffOverrides and repairDestructiveCommandOverrides were merged into repairRuleOverrides, which preserves both directions. resolveSecretDisabledRules is exported and used by the GUI explain command to correctly identify disabled rules instead of treating all overrides as disabled.

Pullfrog  | Fix it ➔View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) (GPT Sol not used — the program covers this model; add its provider key to run your pick) | 𝕏

kenryu42 added 2 commits July 31, 2026 19:48
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.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ 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 update commandcc-safety-net update detects all installed integrations (including legacy-only Claude Code, Codex, and Copilot plugins) and re-runs their install logic with an updating flag. Native integrations use their own update subcommands (plugin marketplace update + plugin update, plugin marketplace upgrade, extensions update) instead of fresh-install commands. The u key in interactive install triggers the update flow.
  • Introduced NativeInstallPlan type with per-integration update flag so each native install command closure decides which commands to run based on whether the plugin already exists.
  • Centralized Copilot plugin directory constantsCOPILOT_PLUGIN_DIR and COPILOT_LEGACY_PLUGIN_DIR moved from local constants in doctor/hooks.ts to exported values in src/integrations/copilot-cli.ts, shared between doctor and install.
  • Added npx cache clearingclearNpxSafetyNetCache() in src/bin/hook/install/npx-cache.ts removes stale npx cache entries for cc-safety-net on config-target installs, ensuring the latest binary is fetched. Correctly handles npm cache location across env vars, platform defaults, and empty values.
  • Added detectInstallHookState shared 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 the update sentinel.

Pullfrog  | Fix it ➔View workflow run | Using 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.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ 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.ts and 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. isDataOnlyQuotedAssignment in src/core/analyze/deferred-assignment.ts walks 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-config destructive 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 unknown routes (src/core/git-metadata-protection.ts) — findGitMetadataMutationTargetInSemanticFacts now also inspects path targets when route.kind === 'unknown', alongside patch and path.
  • Added signature value redaction (src/core/sanitize.ts) — x-amz-signature, x-goog-signature, sig, and signature values 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 into src/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 via CC_SAFETY_NET_NO_UPDATE_CHECK.
  • Fixed rule test false green (src/bin/rule/index.ts) — invoked without arguments, rule test was passing an empty array to testRulebookSources, which silently returned success. Now it reads the actual rule.json config 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/installation anchors.
  • Added SVG favicon for the policy GUI (src/bin/gui/favicon.svg, inlined via data:image/svg+xml in renderPolicyGuiHtml). Mobile CSS extended so nested .tier-collapse heads also wrap on narrow viewports.
  • Added opt-in live E2E test suite (tests/e2e-live/protection.test.ts) — gated behind CC_SAFETY_NET_E2E_LIVE=1 and 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 allowed git status && touch allow-ran, a blocked git reset --hard retaining uncommitted changes, and a blocked .env file read (canary content must not appear in the transcript).

Pullfrog  | Fix it ➔View workflow run | Using 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.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ 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.ts and the Claude Code doctor detection in src/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 vocabularysrc/types.ts deleted; 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 to src/core/analyze/constants.ts. Hook input/output interfaces inlined into each hook file. ValidationResult inlined into src/core/config.ts.
  • Embedded rule metadata into PolicySnapshotsrc/config/policy-metadata.ts removed; ruleMetadata is now an explicit Readonly<Record<string, CustomRuleMetadata>> field populated at snapshot creation. deepFreeze replaces the field-by-field freezePolicy, preventing silently-mutable new fields.
  • Unified normalization pathnormalizeGuiPolicy replaces repairPolicyConfig as the single normalizer from untrusted JSON to the canonical policy-file shape. readPolicyConfig now normalizes through the same path for both valid and salvageable input, replacing the prior getUserPolicySchema().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 with lefthook (lefthook.yml, prepare script). Knip config updated to treat build scripts as entries with the github-actions plugin disabled.
  • Added behavioral contract boundary teststests/core/analyze/behavioral-contract-cases.ts gains 35 new cases covering force-push, stash, find -exec, xargs shell 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 specific ruleId, intent, reasonIncludes, and segment.
  • Fixed status terminal-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 isolates CC_SAFETY_NET_HOME per test file; statusline, status, and explain tests no longer read the developer's real ~/.cc-safety-net.

Pullfrog  | Fix it ➔View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) (GPT Sol not used — the program covers this model; add its provider key to run your pick) | 𝕏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant