Skip to content

feat: git and agent-file checks (node9Firewall#413) - #433

Merged
node9ai merged 9 commits into
mainfrom
dev
Oct 30, 2026
Merged

node9ai merged 9 commits into
mainfrom
dev

Conversation

@node9ai

@node9ai node9ai commented Oct 30, 2026

Copy link
Copy Markdown
Member

Release of node9Firewall#413 (git and agent files), merged to dev in #432.

  • commands.git-destructive is decided from the parsed command (shell/git.ts): a resolved variable, git -C, an alias definition and sh -c are judged by what runs.
  • New check commands.git-hooks-bypass: --no-verify, core.hooksPath, HUSKY=0, and a hook file removed, emptied or made non-executable.
  • New opt-in check commands.git-stage-all.
  • New check files.agent-instructions: writes to CLAUDE.md, AGENTS.md, memory, agents/commands/skills, and changes to the hooks of agent settings. Standard asks only when the text carries an instruction to hide, skip approvals or touch secrets.
  • The history scan's canonical extractor runs the gate's file checks for file tools (canonical-v32, so daemons re-scan once).
  • CI fixes from the PR: the chore(deps-dev): bump tsup from 8.5.1 to 9.0.0 in /packages/policy-engine #413 integration rows name their hook event; readAgentSettings reads through the open descriptor with O_NONBLOCK (CodeQL js/file-system-race); the inert-hook-line check is linear (CodeQL js/polynomial-redos).

CI on #432 was green on Ubuntu, Windows, coverage, CodeQL and e2e.

🤖 Generated with Claude Code

node9ai and others added 9 commits October 30, 2026 08:26
…ode9Firewall#413 part 1)

The two git rules were regexes anchored at the start of the raw text, so a
destructive call written indirectly passed (`cmd="reset --hard"; git $cmd`,
`git config alias.x 'reset --hard' && git x`, `git -C dir reset --hard`,
`env … git`, `timeout 30 git`, `sh -c 'git push -f'`) and four ordinary
commands were stopped (`push --dry-run --force`, `clean -fn`, `branch -d`,
a plain `rebase`).

shell/git.ts reads every git invocation from the shell AST in the scope the
credential jail already walks: variables assigned earlier in the command,
`$(echo …)`, `for` loops over literal items, wrappers, `sh -c` / `eval`
payloads and a heredoc into a shell, functions at their calls with "$@"
bound, git's global options, and aliases defined in the same command
(`git config alias.*`, `git config set`, `-c alias.*`, `!` aliases). An alias
definition is judged by its value; a subcommand that cannot be read is a
review. Force push also covers --force-if-includes, --mirror and +refspec.
Short options bundle as git reads them (`-fn`, `-df`).

Wiring: the AST review is carried to the tier-3 candidates so a user
`block-force-push` rule still wins; a `block` on the check returns at once.
The regex twins join AST_FS_REGEX_RULES and keep speaking for a command
the parser refuses and (in the scan) for a user rule by the same name.
The scan emits the same rule names; canonical-v32.

Measured on this machine's own audit.log (15,483 distinct agent git
commands): old flagged 100, new 89; 55 lose the flag (rebases, branch -d,
git text inside commit messages and heredocs), 44 gain it (reset --hard,
branch -D, force pushes on their own line that the start-anchored regex
never saw). 197 corpus rows written from the issue's categories, 8
integration rows through dist/cli.js.

Design: node9/docs roadmap/active/git-and-agent-files-design.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…oks (node9Firewall#413 part 2)

A new check, Soft log, Standard review, Strict block, for the agent
switching off the git hooks that check its own work (OWASP ASI10):
`commit --no-verify` / `-n` (bundles too), `--no-verify` on push, merge,
rebase, am, cherry-pick and revert; `-c core.hooksPath=…`, `git config
[scope] core.hooksPath …` written, set, added or unset, GIT_CONFIG_PARAMETERS
or GIT_CONFIG_KEY_n naming it; HUSKY=0, HUSKY_SKIP_HOOKS, LEFTHOOK=0 or
SKIP=… applied to the git call as a prefix, through env, or exported
earlier in the command (HUSKY=0 npm ci is not a bypass); chmod -x / 644 on
a hook; and a hook file under .git/hooks or .husky removed, moved away,
truncated or written with text that runs nothing, through the shell or a
file tool. An append and a hook written with real content are not.

The shell half rides the part-1 walk in shell/git.ts, so every indirect
spelling counts; the file half (files/git-hooks.ts) reads the accesses the
collectors already produce, which gain an `append` flag for `>>` and
`tee -a` (files/paths.ts is an extractor source, hence the hash bump; the
version bump of part 1 already covers the re-scan). The policy's git tier
loops over the two check ids, each through its own value; the file verdict
is combined with the protected-file one by strictness inside
evaluatePolicy. A developer running --no-verify from the terminal is not
judged (the AST tier runs for agent calls only).

94 corpus rows, 3 level rows, 5 integration rows through dist/cli.js.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…hing at once (node9Firewall#413 part 3)

A preference, not harm, so off at every level and set only by a person
(node9 checks set, the config `checks` map, the dashboard). Once on, the
walk of part 1 reports `git add -A` / `--all` / `-u` with no path, `git
add .` and any pathspec that is the whole tree (`./`, `src/..`, `:/`,
`:(top)`, `*`), `git commit -a` (bundles too) and `git commit .`; named
files, `-A src/`, `-p`, `-i`, `-n` and `--dry-run` are not reported. The
same resolution applies, so an alias (`git config alias.ca 'commit -a'`)
or a `sh -c` spelling counts. AgentVerdict's git-guardrails profile
(`gitPolicies: stage-by-name`) can map to this check instead of a tuned
user rule.

57 corpus rows, a policy-level test of the default and the three values,
one integration row with the check set in a v2 config file.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…truction and memory files (node9Firewall#413 part 4)

A new check, Soft log, Standard review, Strict review on every write, for
CLAUDE.md, CLAUDE.local.md, AGENTS.md, GEMINI.md, .cursorrules,
.cursor/rules/*, .github/copilot-instructions.md, the memory directory,
.claude/agents|commands|skills (project or home) and the hooks of the
agent settings files (~/.claude/settings.json, .claude/settings*.json,
~/.gemini/settings.json, ~/.codex/hooks.json, ~/.cursor/hooks.json).
These files are loaded into every later session, so a line written there
persists (OWASP ASI06); a rewritten hook disarms the guard itself (ASI10).

At Standard every write is recorded and the call is held only when:
- the written text carries an instruction to hide something from the
  user, to skip approvals or disarm the guard, or to read or send a secret
  (dlp/instruction-write.ts: three new signals plus the output scanner's
  override, fake-role and destination signals, one enough; an imperative
  context required, a negation in front cancels, quoted mentions and code
  spans or fenced blocks are left alone, the destination must be real);
- the hooks of a settings file change: a Write is compared with the file
  on disk, an Edit is applied to it first (host hook readAgentSettings,
  256 KiB cap); without the file, any mention of hooks counts;
- a shell write's content cannot be read from the command (cp, mv, a
  download), or the file is removed.
Strict and Block all ask on every write (levels.ts
reviewsEveryAgentFileWrite). The file verdict is combined with the
protected-file and hook-file verdicts by strictness inside evaluatePolicy;
fw#318 P4 (a write after outside content was read) is untouched.

Measured before the patterns joined: 338 of our own agent files (every
CLAUDE.md, AGENTS.md and memory file on the development machine,
third-party AGENTS.md included, 1.47 MB); the first draft matched 173
times, the shipped rule 6 files, listed in the design. The legitimate
corpus test ships our own instruction files as fixtures. 141 engine rows,
5 level rows, 3 integration tests through dist/cli.js with a real
settings file in a temp HOME.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Seven of ten findings from /code-review high on the four commits:

- A shell write to an agent file whose text is in the command (`echo … >
  CLAUDE.md`, a heredoc, printf, sed -i, touch, `: > F`) was reviewed as
  "content cannot be read". shell/write-source.ts reads the statement:
  only a copy verb (cp, mv, install, rsync, dd, a download, tee or cat
  fed by a file) is unseen; a command that does not parse stays unseen.
- `git config --global alias.st` (a read of the alias) was judged as a
  git call with an unreadable subcommand; a definition needs a value.
- `git -c clean.requireForce=false clean -d` passed: the config the walk
  already reads now counts as the force.
- The narrowed destination regex replaced the output scanner's by array
  position; replaced by what it matches instead.
- The quoted-mention filter was copied from injection.ts: isMention is
  exported and shared.
- resolveShellInput was computed three times per call: once.
- A logged file finding replaced a logged shell finding in the one audit
  row: the command's own finding is kept.

Left as they are, with the reason in the design (7g): the scan's missing
file-tool half (a separate piece of work), the silent catch in
GitWalk.run (a throw would fail the hook open), and the host's own
writtenContent in fw#318 P4 (untouched by this issue on purpose).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…irewall#413 review, finding 5)

extractCanonicalFindings had no file-tool half for the file checks the gate
runs, so a Write that empties .husky/pre-commit, an Edit of
~/.claude/settings.json that replaces a hook command, or a Write of "do not
tell the user" into CLAUDE.md produced no extractor finding while the live
gate held each of them.

- files/file-checks.ts: evaluateFileChecks runs the protected-file, git-hooks
  and agent-file checks and keeps the strictest verdict. evaluatePolicy and
  the extractor both call it.
- scan/canonical.ts: file tools emit a 'file-access' finding with the gate's
  ruleName and a new checkId field. ExtractContext gains cwd, homeDirs and
  catalogSettings. A logged allow is not a finding; the finding keeps only
  the path, not the written content.
- files/agent-files.ts: settingsUnknown (set by the scan, which has no
  settings reader) judges a whole settings write by whether its hooks run
  nothing, instead of reporting every write of a file that has hooks.
- daemon/scan-watermark.ts: canonicalContextForLine passes the line's
  absolute cwd and the home directory.
- canonical-v32 log entry with the measured rows; the hashed source set now
  covers shell/git.ts and the file-check sources; hash regenerated.

The CLI `node9 scan` already reported these calls (it judges through the
gate). Not covered yet: shell writes to the same files, and toScanFinding
still drops 'file-access' before upload.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…line check

CI on PR #432 failed on Ubuntu, Windows and CodeQL:

- check.integration: the #413 payloads carried no hook_event_name, so the
  agent was recognised only from CLAUDECODE in the environment. Locally the
  tests ran inside Claude Code and passed; on CI the review waited for an
  approver until the spawn was killed (status null). Every payload now names
  its hook event.
- scan-watermark-file-context: the absolute-path row is POSIX only. The file
  checks match paths on `/`, and the gate's own context passes os.homedir()
  the same raw way, so on Windows neither side matches.
- readAgentSettings (CodeQL js/file-system-race): type and size now come
  from the open descriptor through readCappedText, which gains an optional
  open-flags argument; O_NONBLOCK keeps a FIFO at the path from hanging the
  gate. New spec, including the FIFO row (hangs without the flag).
- hookTextRunsNothing (CodeQL js/polynomial-redos): the line is trimmed and
  the pattern no longer has `\s*` on both sides of an optional group. A
  50,000-space line took 715 ms before; the new row bounds it at 200 ms.

Extractor hash regenerated (git-hooks.ts is in the hashed set); detector
output is unchanged, so canonical-v32 stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Windows the host's file layer is off: prepareFileAccesses
(src/policy/file-access.ts) returns no accesses on win32, so the hook-file
and agent-file checks never run there, like the older protected-file
checks. The four file-tool rows and `rm .husky/pre-commit` now skip on
Windows (itUnix, the pattern other integration specs use); the
`--no-verify` and `HUSKY=0` rows come from the command parser and still
run everywhere.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat: git and agent-file checks (node9Firewall#413) and their file-tool half in the scan
@github-actions

Copy link
Copy Markdown
Contributor

🛡️ node9 agent-security · ✅

This PR introduces no agent-security findings.

0 finding(s) · · technical details

@node9ai
node9ai merged commit e4bc8b4 into main Oct 30, 2026
13 checks passed
@node9ai

node9ai commented Oct 30, 2026

Copy link
Copy Markdown
Member Author

🎉 This PR is included in version 2.42.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant