Skip to content

feat: steering-first surface — change-17 user-input phase, change-18 QoL steering packs, change-7 claude-code adapter - #15

Open
nimser wants to merge 1 commit into
mainfrom
feat/steering-first-direction
Open

feat: steering-first surface — change-17 user-input phase, change-18 QoL steering packs, change-7 claude-code adapter#15
nimser wants to merge 1 commit into
mainfrom
feat/steering-first-direction

Conversation

@nimser

@nimser nimser commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Part 2 of 2, stacked on #16 (feat/new-direction-alignment) — merge that first. Implements the three active openspec changes (change-18, change-17, change-7).

What's here

  • Nine built-in rule packs, 25 rules (src/packs/): steering — modern-cli, package-manager, git-safety; security — env, sops, private-key, secret-managers, encryption-tools, hardening. Built-in predicates (npm-in-pnpm-repo, on-protected-branch, cat-large-file) and loadBuiltInRulePacks() as the zero-config path. Positive and negative match cases tested.
  • user-input phase wired end to end: engine phase dispatch, prompt-text matching (never command-split), redactUserInput capability gate with redact → block fallback.
  • Claude Code hook adapter (src/adapters/claude-code/): structured permissionDecision JSON only (no exit-code-2), updatedInput for run, native ask for confirm, UserPromptSubmit scrubbing, unconditional fail-closed wrapper (ADR-007).
  • Auto-generated docs/packs.md gallery + CI staleness check.

Known gaps before this is user-ready (acceptance criteria for follow-ups)

  1. Packaging: YAML packs aren't copied into dist/ by the tsc build and yaml is a devDependency — a published package would be broken. Fix before any release.
  2. No CLI installer (npx agent-guardrails install …, change-12) and no bin entry — the adapter's main() can't reach anyone's hook config yet.
  3. Hook schema unverified: the updatedPrompt/updatedInput/UserPromptSubmit response shapes are written from research notes and need a smoke test against a live Claude Code before relying on them.
  4. No Pi adapter (change-3) — the direction's primary harness still has nothing.
  5. After-tool redact evaluation is still a TODO in the engine, so output-redaction rules can't fire; the user-input redact flow should also move the prompt rewrite into the engine instead of re-matching in the adapter.

236 tests green; lint/format/typecheck/build pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FUpa7w5hYzrXyCYkK8K1DC

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

This PR adds user-input phase handling to the engine and matcher, introduces nine built-in YAML rule packs (git-safety, modern-cli, package-manager, encryption-tools, env, hardening, private-key, secret-managers, sops) with supporting predicates and a pack loader exported from the package index, adds a Claude Code hook adapter (normalization, response translation, engine wiring, CLI entry) with tests, and adds a gallery-generation script producing docs/packs.md plus a CI step that verifies the doc stays synchronized with pack definitions.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Stdin
  participant main
  participant handleHookEvent
  participant normalizeHookPayload
  participant Engine
  participant toHookResponse
  Stdin->>main: raw JSON payload
  main->>handleHookEvent: rawPayload, engine
  handleHookEvent->>normalizeHookPayload: payload
  normalizeHookPayload-->>handleHookEvent: ToolCallContext | null
  handleHookEvent->>Engine: evaluate(ctx)
  Engine-->>handleHookEvent: GuardrailAction | null
  handleHookEvent->>toHookResponse: action, payload
  toHookResponse-->>main: ClaudeCodeHookResponse
  main-->>Stdin: JSON response
Loading

Related PRs: None identified from the provided context.

Suggested labels: enhancement, documentation, ci

Suggested reviewers: None identified from the provided context.

Poem:
A rabbit hopped through packs of nine,
Blocked the secrets, drew the line,
Claude Code hooks now parse and guard,
Gallery docs regenerate — not hard!
Thump thump, the CI checks align. 🐇

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The description matches the changeset, covering the new packs, user-input phase, Claude Code adapter, docs gallery, and CI check.
✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch feat/steering-first-direction

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 24

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
docs/rule-pack-guide.md (1)

202-210: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Scope this override example to overridable rules.

The docs above say built-in rules can be locked with overridable: false, so “any built-in rule” is inaccurate here. Narrow the example to user-overridable rules only.

♻️ Proposed fix
-You can disable or change any built-in rule in `agent-guardrails.json`:
+You can disable or change any overridable built-in rule in `agent-guardrails.json`:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/rule-pack-guide.md` around lines 202 - 210, The override example in the
rule pack guide is too broad because it says “any built-in rule” even though
some built-in rules can be locked with overridable: false. Update the example
text near the agent-guardrails.json snippet to refer only to user-overridable or
overridable built-in rules, keeping the example aligned with the documented
behavior.
src/index.ts (1)

36-46: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Stale "both phases" doc comments after adding the user-input phase.

The re-exported JSDoc for GuardrailAction ("Union of all possible guardrail actions across both phases") and GuardrailRule ("Union of rules from both phases") wasn't updated when the user-input phase was added. src/core/types.ts already updated its own doc comments on these same types to say "all phases" — these public re-export comments in the barrel file are now inconsistent with the source and understate the union (it's before-tool/after-tool/user-input, i.e., three phases, not two).

📝 Proposed fix
-  /** Union of all possible guardrail actions across both phases. */
+  /** Union of all possible guardrail actions across all phases. */
   GuardrailAction,
   /** Normalized context for a tool call. */
   ToolCallContext,
   /** A declarative condition that describes when a rule fires. */
   MatchCondition,
-  /** Union of rules from both phases. */
+  /** Union of rules from all phases. */
   GuardrailRule,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/index.ts` around lines 36 - 46, Update the barrel re-exports in
src/index.ts so the JSDoc for GuardrailAction and GuardrailRule matches the
current phase model; the comments still say “both phases” and are stale after
adding user-input. Align these public docs with the source types in core types
by changing the wording to refer to all phases, so the exported API comments
correctly reflect before-tool, after-tool, and user-input.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 25-26: The check job in the CI workflow is using the default
unscoped GITHUB_TOKEN permissions, so add an explicit least-privilege
permissions block for this job or at the workflow level. Update the workflow
around the check job and the new docs/packs.md verification step so it only
requests the minimal read-only access needed, with no write permissions to
contents, issues, or pull requests.

In `@docs/adrs/002-behavior-model.md`:
- Around line 23-33: The behavior model text incorrectly says `block` and
`suggest` work everywhere, which conflicts with the phase matrix and
`validateRule()`. Update the ADR wording in the section defining the three
phases so `block` is described as the safe baseline across all phases, while
`suggest` is only mentioned for `before-tool`; keep the `user-input` and
`after-tool` allowed behaviors aligned with the existing matrix and ADR-010.

In `@docs/adrs/003-public-api-contract.md`:
- Around line 29-32: The public contract entry for Engine.evaluate should not
mention ctx.phase because phase is derived internally from toolName and is not
part of the runtime API shape. Update the ADR table row for Engine.evaluate to
describe the method as taking ToolCallContext without exposing phase, and keep
the wording aligned with the Engine.evaluate and ToolCallContext symbols so the
contract matches the implementation.
- Around line 17-25: The bootstrap example in the public API contract is not
self-contained because it uses registry before defining it. Update the snippet
around createEngine and loadAllRulePacks to either include the registry
initialization in the example or remove the custom-registry argument from
loadAllRulePacks so the code can be pasted and run as shown.

In `@docs/adrs/010-user-input-mediation.md`:
- Around line 27-33: The user-input contract description is inconsistent with
the implementation and should be updated to match the actual symbols and shape
used by ToolCallContext and HarnessCapabilities. In the ADR, replace the “phase:
user-input” wording with the real tool context field naming (`toolName:
'user-input'`) and describe the raw prompt as living in `command`; also note
that `redactUserInput` is optional and defaults to false as defined in
HarnessCapabilities. Keep the matcher guidance aligned with the actual contract
so adapter authors are not told that bash-command matchers are excluded if that
is not true.

In `@README.md`:
- Around line 44-49: The README example uses loadAllRulePacks("./packs",
registry) before defining registry, so make the snippet copy-paste runnable by
adding the PredicateRegistry setup in the example or replacing it with a
self-contained placeholder-free version. Update the
createEngine/loadAllRulePacks example so registry is introduced before it is
passed in, keeping the sample focused on the README usage shown in the import
and engine.evaluate flow.

In `@src/adapters/claude-code/adapter.test.ts`:
- Around line 1-169: Add tests to cover the untested startup failure path in
main() by making createClaudeCodeEngine() or pack loading throw and asserting
the startup behavior is fail-closed as expected, and extend
normalizeHookPayload() coverage for Write/Edit/Read inputs that omit file_path
so the normalized output explicitly handles the missing path case; use the
existing adapter.test.ts patterns around handleHookEvent,
createClaudeCodeEngine, and normalizeHookPayload to place the new cases.

In `@src/adapters/claude-code/hook.ts`:
- Around line 33-55: `main()` currently leaves stdin reading and
`createClaudeCodeEngine()` outside any fail-closed handling, so errors can
escape without producing a block response. Wrap the full CLI flow in
`main()`—including the `for await (const chunk of process.stdin)` loop,
`createClaudeCodeEngine`, and `handleHookEvent`—in a top-level try/catch. On any
exception, write the same structured blocked decision JSON to stdout before
exiting, so the Claude Code hook always fails closed.
- Around line 47-51: The stdin read in main currently waits indefinitely for
EOF, which can stall the hook when input stays open. Add a timeout around the
process.stdin chunk collection so the read fails closed if the payload does not
arrive in time, and make sure the timeout path aborts the hook cleanly with a
clear error.

In `@src/adapters/claude-code/respond.ts`:
- Around line 68-94: The inline comment in userPromptResponse is hard to
understand and should be rewritten for clarity. Update the note around the
redact branch in userPromptResponse to plainly state that user-input only
supports redact, block, and confirm, and that scrubPrompt is applied here when
payload.prompt is present. Keep the explanation brief and directly tied to the
redact case so the intent is obvious when reading the ClaudeCodeHookResponse
mapping.

In `@src/core/types.ts`:
- Around line 30-33: The confirm fallback in UserInputAction still allows
BeforeToolAction recursively, which lets suggest/run actions slip through
user-input validation. Update the UserInputAction definition and the
isUserInputAction() validation path to narrow the nested confirm.fallback type
for user-input rules, or explicitly reject suggest and run when validating
confirm fallbacks. Use the UserInputAction and isUserInputAction symbols to
locate the affected logic.

In `@src/core/validator.ts`:
- Around line 59-65: The user-input confirm fallback is too permissive because
isUserInputAction still routes non-redact actions through isBeforeToolAction,
which lets suggest/run and nested confirm slip through. Update the validation
logic in isUserInputAction to treat confirm.fallback as block-only for
user-input actions, and make malformed fallback shapes fail validation at load
time rather than being accepted by isBeforeToolAction. Keep the fix localized to
the validator flow around isUserInputAction and any confirm-related type checks
it relies on.

In `@src/engine/engine.ts`:
- Around line 51-53: The nested ternary in the command derivation logic is hard
to read and should be refactored for clarity. Extract the logic around the
existing commands assignment in engine.ts into a small helper such as
deriveCommands(ctx, command) or replace it with an if/else chain, preserving the
user-input special case and the splitCommands fallback behavior. Keep the new
logic close to the current command handling in engine/engine.ts so it remains
easy to test and reuse.

In `@src/matcher/matchers.ts`:
- Around line 36-38: Add a matcher test in matchers.test.ts that explicitly
exercises the widened bash-command branch in matchers.ts for ctx.toolName values
of both bash and user-input. Verify that user-input with a command matching the
bash-command pattern is accepted, and include a case where the prompt exceeds
MAX_MATCH_INPUT_LENGTH so the guard still returns false.

In `@src/packs/git-safety.yaml`:
- Around line 18-31: The git-safety.commit-to-main rule is missing the {matched}
placeholder in both its confirmation and block messages, unlike the other
predicate-based rules in this YAML. Update the defaultAction.message and
fallback.message in the commit-to-main entry so they interpolate {matched},
keeping the wording consistent with the other rules and making the matched git
commit command visible to the user.

In `@src/packs/hardening.yaml`:
- Line 12: The hardening wrapper patterns are too broad and match unrelated
filenames because `hardening.eval`, `hardening.shell-c`, and
`hardening.substitution` use raw substrings like `.env`, `.pem`, and `.key`.
Tighten the regexes in the hardening config so these branches only match real
secret/file suffixes using word boundaries or equivalent delimiters, and make
the `sops`-related matching consistent across the three patterns.

In `@src/packs/index.ts`:
- Around line 9-18: The built-in packs are not being shipped with the compiled
package, so `loadBuiltInRulePacks()` ends up loading nothing because
`BUILTIN_PACKS_DIR` points at `dist/packs` while the YAML files stay under
source. Update the build/publish flow to copy the built-in YAML assets alongside
the compiled output in `dist/packs`, and make sure the packaging step includes
them so `loadAllRulePacks()` can find the shipped rule packs at runtime.

In `@src/packs/modern-cli.yaml`:
- Around line 6-16: The `modern-cli.grep` matcher only catches recursive `grep`
flags containing a lowercase `r`, so it misses uppercase `-R` invocations.
Update the bash-command pattern in the `modern-cli.grep` rule to also match `-R`
(while keeping the existing `--recursive` coverage) so calls like `grep -R ...`
trigger the same `rg` suggestion.
- Around line 18-28: The bash-command matcher in modern-cli’s `modern-cli.find`
rule is too restrictive because it only matches `find` invocations with an
explicit path before `-name`. Update the `pattern` so it also matches the common
path-omitted form like `find -name "*.log"` while still catching existing cases,
and keep the `defaultAction` suggestion behavior unchanged.

In `@src/packs/packs.test.ts`:
- Around line 59-92: Add adversarial coverage in packs.test around the matcher
gaps mentioned in the review: extend the existing makeEngine/bash/read
assertions to include positive blocks for FIDO2 private keys like id_ed25519_sk
and id_ecdsa_sk, and add negative/positive cases for hardening.yaml boundary
behavior such as .keys and .environment false positives plus
.ssh/authorized_keys and .ssh/config handling. Use the existing engine.evaluate,
bash, and read tool patterns in packs.test.ts so the tests clearly pin the regex
behavior and prevent regressions.

In `@src/packs/predicates.ts`:
- Line 7: The GIT_COMMIT matcher in predicates.ts is too broad and can fire for
git commit-* plumbing commands like commit-graph or commit-tree. Tighten the
pattern used by onProtectedBranch so it matches only the porcelain git commit
command, not subcommands with a hyphen after commit. Update the GIT_COMMIT regex
in the predicates logic and verify the confirm/block path in the commit-to-main
rule no longer triggers for those unrelated commands.
- Around line 17-20: The npmInPnpmRepo predicate only checks the current cwd for
pnpm-lock.yaml, so it misses pnpm workspaces when the command is run from a
package subdirectory. Update npmInPnpmRepo to walk up from cwd toward the repo
root until it finds pnpm-lock.yaml (or reaches the filesystem root), and use
that result to decide whether to return true, keeping the existing NPM_INSTALL
and command(ctx) check intact.

In `@src/packs/private-key.yaml`:
- Around line 6-16: The bash-command matcher in private-key.cat does not cover
OpenSSH FIDO2 key filenames, so update the pattern in the rule to also match the
`id_ed25519_sk` and `id_ecdsa_sk` variants alongside the existing `id_*` cases.
Keep the change localized to the `match.pattern` in this rule so shell reads
like `cat`, `less`, `head`, and `tail` are blocked consistently for both
traditional private keys and security-key-backed private keys.
- Around line 22-23: The private-key rule in the private-key pack is too broad
under `.ssh` because the current pattern blocks non-secret files like
`known_hosts`, `config`, and `authorized_keys`. Tighten the pattern in the
file-path matcher so it only targets actual private-key material, and keep the
existing `.pem`/`.key` coverage while narrowing the `.ssh` path handling in the
same rule.

---

Outside diff comments:
In `@docs/rule-pack-guide.md`:
- Around line 202-210: The override example in the rule pack guide is too broad
because it says “any built-in rule” even though some built-in rules can be
locked with overridable: false. Update the example text near the
agent-guardrails.json snippet to refer only to user-overridable or overridable
built-in rules, keeping the example aligned with the documented behavior.

In `@src/index.ts`:
- Around line 36-46: Update the barrel re-exports in src/index.ts so the JSDoc
for GuardrailAction and GuardrailRule matches the current phase model; the
comments still say “both phases” and are stale after adding user-input. Align
these public docs with the source types in core types by changing the wording to
refer to all phases, so the exported API comments correctly reflect before-tool,
after-tool, and user-input.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: dc6f663f-38f2-45c3-a41a-156d76b30174

📥 Commits

Reviewing files that changed from the base of the PR and between 781ae79 and b577724.

📒 Files selected for processing (46)
  • .github/workflows/ci.yml
  • .gitignore
  • CONTRIBUTING.md
  • LIMITATIONS.md
  • README.md
  • SECURITY.md
  • docs/adrs/001-layered-architecture.md
  • docs/adrs/002-behavior-model.md
  • docs/adrs/003-public-api-contract.md
  • docs/adrs/005-yaml-rule-packs.md
  • docs/adrs/006-observability-strategy.md
  • docs/adrs/007-trust-and-self-protection.md
  • docs/adrs/008-non-goals.md
  • docs/adrs/009-adapter-scope-and-tiering.md
  • docs/adrs/010-user-input-mediation.md
  • docs/getting-started.md
  • docs/packs.md
  • docs/rule-pack-guide.md
  • package.json
  • scripts/generate-packs-gallery.mjs
  • src/adapters/claude-code/adapter.test.ts
  • src/adapters/claude-code/hook.ts
  • src/adapters/claude-code/normalize.ts
  • src/adapters/claude-code/respond.ts
  • src/core/harness-capabilities.ts
  • src/core/normalizer.ts
  • src/core/types.ts
  • src/core/validator.ts
  • src/engine/create-engine.ts
  • src/engine/engine.ts
  • src/index.ts
  • src/matcher/matchers.ts
  • src/packs/encryption-tools.yaml
  • src/packs/env.yaml
  • src/packs/git-safety.yaml
  • src/packs/hardening.yaml
  • src/packs/index.ts
  • src/packs/modern-cli.yaml
  • src/packs/package-manager.yaml
  • src/packs/packs.test.ts
  • src/packs/predicates.ts
  • src/packs/private-key.yaml
  • src/packs/secret-managers.yaml
  • src/packs/sops.yaml
  • src/resolver/action-resolver.test.ts
  • src/resolver/action-resolver.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (9)
**

⚙️ CodeRabbit configuration file

**: # Agent Guardrails

⚠️ Heavy development: Not yet published to npm. Expect breakage. The quick start below shows the expected workflow once the package is available.


npm
Build
Coverage
Bundle Size
TypeScript
License: MIT
PRs Welcome

Quality Gate Status
Coverage

What is this?

A pattern-based steering engine for AI coding agent workflows.
Keep your agent shipping — not spilling secrets no one's watching for. Agent Guardrails steers agents away from costly mistakes instead of just stopping them cold.

Is this just a set of guards to block bad behaviour?

No — mostly it transforms. Wherever a Safer Alternative exists, that's the default: reading .env or decrypting a SOPS file resolves to suggest, handing the agent a redacted command it can retry immediately instead of stalling out. block is what's left for the cases with nothing saf...

Files:

  • src/matcher/matchers.ts
  • docs/adrs/010-user-input-mediation.md
  • src/packs/package-manager.yaml
  • docs/adrs/009-adapter-scope-and-tiering.md
  • src/packs/modern-cli.yaml
  • docs/adrs/005-yaml-rule-packs.md
  • src/packs/index.ts
  • src/packs/secret-managers.yaml
  • scripts/generate-packs-gallery.mjs
  • src/packs/git-safety.yaml
  • src/packs/env.yaml
  • src/packs/private-key.yaml
  • src/packs/packs.test.ts
  • src/engine/create-engine.ts
  • src/adapters/claude-code/hook.ts
  • src/packs/encryption-tools.yaml
  • src/packs/sops.yaml
  • src/adapters/claude-code/normalize.ts
  • docs/packs.md
  • SECURITY.md
  • src/core/normalizer.ts
  • LIMITATIONS.md
  • src/packs/hardening.yaml
  • src/adapters/claude-code/adapter.test.ts
  • src/packs/predicates.ts
  • src/resolver/action-resolver.ts
  • package.json
  • src/adapters/claude-code/respond.ts
  • CONTRIBUTING.md
  • docs/adrs/006-observability-strategy.md
  • src/resolver/action-resolver.test.ts
  • src/core/harness-capabilities.ts
  • docs/rule-pack-guide.md
  • docs/adrs/008-non-goals.md
  • src/core/validator.ts
  • docs/adrs/001-layered-architecture.md
  • docs/getting-started.md
  • docs/adrs/007-trust-and-self-protection.md
  • src/engine/engine.ts
  • docs/adrs/002-behavior-model.md
  • docs/adrs/003-public-api-contract.md
  • src/index.ts
  • README.md
  • src/core/types.ts
src/matcher/**/*.ts

⚙️ CodeRabbit configuration file

src/matcher/**/*.ts: Flag if: imports from resolver/, engine/, or infrastructure/; side effects or global state in matching logic; missing adversarial test cases for new regex patterns; reintroducing a registry/handler split.
Do NOT flag: command-splitting limitations (variable tracking is explicitly out of scope); the MatchCondition discriminated union being a closed set evaluated by a single matchesMatcher() function.

Files:

  • src/matcher/matchers.ts
docs/**/*.md

⚙️ CodeRabbit configuration file

Flag if: broken relative links; code examples that don't match actual API signatures; incorrect vocabulary usage (see README.md vocabulary table — Behavior≠Action, RulePack≠package, Harness≠Agent).

Files:

  • docs/adrs/010-user-input-mediation.md
  • docs/adrs/009-adapter-scope-and-tiering.md
  • docs/adrs/005-yaml-rule-packs.md
  • docs/packs.md
  • docs/adrs/006-observability-strategy.md
  • docs/rule-pack-guide.md
  • docs/adrs/008-non-goals.md
  • docs/adrs/001-layered-architecture.md
  • docs/getting-started.md
  • docs/adrs/007-trust-and-self-protection.md
  • docs/adrs/002-behavior-model.md
  • docs/adrs/003-public-api-contract.md
src/**/*.yaml

⚙️ CodeRabbit configuration file

YAML rule pack files. Flag if: non-kebab-case pack id; non-dotted rule id; phase/action mismatch (before-tool: block/suggest/run/confirm, after-tool: redact only); missing {matched} template in messages; suggest action without replacement.

Files:

  • src/packs/package-manager.yaml
  • src/packs/modern-cli.yaml
  • src/packs/secret-managers.yaml
  • src/packs/git-safety.yaml
  • src/packs/env.yaml
  • src/packs/private-key.yaml
  • src/packs/encryption-tools.yaml
  • src/packs/sops.yaml
  • src/packs/hardening.yaml
src/**/*.test.ts

⚙️ CodeRabbit configuration file

Flag if: snapshot tests for regex patterns (use inline values); missing adversarial/edge-case coverage for matchers; jest imports (this project uses vitest).

Files:

  • src/packs/packs.test.ts
  • src/adapters/claude-code/adapter.test.ts
  • src/resolver/action-resolver.test.ts
src/engine/**/*.ts

⚙️ CodeRabbit configuration file

Flag if: imports from infrastructure/; orchestration growing beyond ~80 lines (business logic belongs in resolver/ or matcher/); non-fail-closed handling of malformed ToolCallContext.

Files:

  • src/engine/create-engine.ts
  • src/engine/engine.ts
src/core/**/*.ts

⚙️ CodeRabbit configuration file

src/core/**/*.ts: Flag if: imports from matcher/, resolver/, engine/, or infrastructure/; use of any or ! non-null assertion; default exports.
Do NOT flag: the five-value GuardrailBehavior union, the phase-behavior matrix, or fail-at-load-time validation — these are intentional design decisions documented in README.md.

Files:

  • src/core/normalizer.ts
  • src/core/harness-capabilities.ts
  • src/core/validator.ts
  • src/core/types.ts
src/resolver/**/*.ts

⚙️ CodeRabbit configuration file

src/resolver/**/*.ts: Flag if: imports from matcher/, engine/, or infrastructure/; missing GuardrailAction variant in switch/exhaustive check; new behavior added without updating both core/types.ts and resolveAction().
Do NOT flag: the confirm→suggest→block fallback chain — it is correct as implemented. Do NOT suggest changing the yaml package to js-yaml — this project uses the yaml package (v2.4.0).

Files:

  • src/resolver/action-resolver.ts
  • src/resolver/action-resolver.test.ts
src/index.ts

⚙️ CodeRabbit configuration file

src/index.ts: Flag if: default exports; new exports without corresponding documentation.
Do NOT flag: re-exports from infrastructure/. The barrel may re-export from any layer.

Files:

  • src/index.ts
🪛 GitHub Check: SonarCloud Code Analysis
src/adapters/claude-code/respond.ts

[warning] 97-97: Use concise character class syntax '\w' instead of '[A-Za-z0-9_]'.

See more on https://sonarcloud.io/project/issues?id=nimser_agent-guardrails&issues=AZ8zJbjppHPotZ9rhgbz&open=AZ8zJbjppHPotZ9rhgbz&pullRequest=15

src/engine/engine.ts

[warning] 53-53: Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=nimser_agent-guardrails&issues=AZ8zJbg-pHPotZ9rhgby&open=AZ8zJbg-pHPotZ9rhgby&pullRequest=15

🪛 LanguageTool
docs/adrs/009-adapter-scope-and-tiering.md

[style] ~27-~27: Consider an alternative for the overused word “exactly”.
Context: ...gin versus out-of-process subprocess is exactly the asymmetry the capability model and ...

(EXACTLY_PRECISELY)

docs/adrs/002-behavior-model.md

[style] ~94-~94: Consider an alternative for the overused word “exactly”.
Context: ...nues. An adversarial wrapper attempt is exactly the case where letting the model "adapt...

(EXACTLY_PRECISELY)

🪛 zizmor (1.26.1)
.github/workflows/ci.yml

[warning] 10-26: overly broad permissions (excessive-permissions): default permissions used due to no permissions: block

(excessive-permissions)

🔇 Additional comments (35)
src/adapters/claude-code/normalize.ts (1)

30-36: 🗄️ Data Integrity & Integration

Malformed read/write payloads are fail-closed in the engine before matching, so this adapter can’t propagate an invalid context downstream.

			> Likely an incorrect or invalid review comment.
docs/adrs/001-layered-architecture.md (1)

40-70: LGTM!

docs/adrs/002-behavior-model.md (1)

56-66: 🎯 Functional Correctness

Verify the Claude Code version floor.

The >= 2.1.121 gate is a version-specific claim about an external hook API. Please confirm it against the adapter docs/schema before freezing it in prose.

LIMITATIONS.md (1)

1-40: LGTM!

README.md (1)

22-43: LGTM!

Also applies to: 51-120

SECURITY.md (1)

1-59: LGTM!

docs/getting-started.md (1)

3-7: LGTM!

Also applies to: 31-74, 155-168, 187-190, 203-203

docs/rule-pack-guide.md (1)

42-198: LGTM!

src/core/types.ts (1)

54-63: LGTM!

Also applies to: 86-97, 140-153

src/core/validator.ts (1)

4-10: LGTM!

Also applies to: 115-115, 127-141

src/core/normalizer.ts (1)

7-15: LGTM!

Also applies to: 51-66

src/core/harness-capabilities.ts (2)

52-60: Custom compareVersions handles the common cases correctly.

Parses dotted numeric segments and pads missing segments with 0, which correctly orders versions like 2.1.121 vs 2.1.120 or 2.1 vs 2.1.121. No issues found for the version formats used here (x.y.z).


1-50: LGTM!

src/index.ts (1)

1-33: LGTM!

Also applies to: 73-119

src/engine/create-engine.ts (1)

1-59: LGTM!

src/engine/engine.ts (3)

122-142: LGTM!


148-165: LGTM!


104-109: 🩺 Stability & Availability

No action: after-tool contexts don’t reach this path

normalizeHookPayload() returns null for PostToolUse, and processMatch() only branches on user-input vs. before-tool, so there’s no current after-tool context flowing into this matcher.

			> Likely an incorrect or invalid review comment.
src/resolver/action-resolver.ts (2)

31-33: LGTM!


138-175: LGTM!

src/resolver/action-resolver.test.ts (3)

176-193: LGTM!


226-237: LGTM!


281-297: LGTM!

src/packs/predicates.ts (1)

1-14: LGTM!

Also applies to: 34-55

src/packs/modern-cli.yaml (1)

30-40: LGTM!

src/packs/package-manager.yaml (1)

1-17: LGTM!

src/packs/git-safety.yaml (1)

6-17: LGTM!

Also applies to: 32-45

src/packs/env.yaml (1)

1-51: LGTM!

src/packs/secret-managers.yaml (1)

1-49: LGTM!

src/packs/sops.yaml (1)

1-28: LGTM!

src/packs/encryption-tools.yaml (1)

1-38: LGTM!

src/packs/packs.test.ts (1)

1-58: LGTM!

Also applies to: 94-174

scripts/generate-packs-gallery.mjs (1)

1-51: LGTM!

package.json (1)

43-44: LGTM!

docs/packs.md (1)

1-98: LGTM!

Comment thread .github/workflows/ci.yml
Comment on lines +25 to +26
- name: Verify pack gallery is up to date
run: npm run docs:packs && git diff --exit-code docs/packs.md

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

check job runs with default (unscoped) GITHUB_TOKEN permissions.

Static analysis (zizmor) flags this job for lacking an explicit permissions: block. None of the steps in this job — including the new gallery-verification step — need write access to the repo, issues, PRs, etc. Add least-privilege permissions at the job or workflow level.

🔒 Proposed fix
   check:
     name: Lint · Format · TypeCheck
     runs-on: ubuntu-latest
+    permissions:
+      contents: read
     steps:

As per static analysis hints: "overly broad permissions (excessive-permissions): default permissions used due to no permissions: block".

🧰 Tools
🪛 zizmor (1.26.1)

[warning] 10-26: overly broad permissions (excessive-permissions): default permissions used due to no permissions: block

(excessive-permissions)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 25 - 26, The check job in the CI
workflow is using the default unscoped GITHUB_TOKEN permissions, so add an
explicit least-privilege permissions block for this job or at the workflow
level. Update the workflow around the check job and the new docs/packs.md
verification step so it only requests the minimal read-only access needed, with
no write permissions to contents, issues, or pull requests.

Source: Linters/SAST tools

Comment thread docs/adrs/002-behavior-model.md
Comment thread docs/adrs/003-public-api-contract.md
Comment thread docs/adrs/003-public-api-contract.md
Comment thread docs/adrs/010-user-input-mediation.md
Comment thread src/packs/packs.test.ts
Comment on lines +59 to +92
it('blocks private key reads via bash and read tool', () => {
const { engine } = makeEngine()
expect(engine.evaluate(bash('cat ~/.ssh/id_rsa'))?.type).toBe('block')
expect(engine.evaluate({ toolName: 'read', filePath: '/home/u/.ssh/id_ed25519' })?.type).toBe(
'block'
)
})

it('allows reading SSH public keys', () => {
const { engine } = makeEngine()
expect(
engine.evaluate({ toolName: 'read', filePath: '/home/u/.ssh/id_ed25519.pub' })
).toBeNull()
})

it('blocks secret manager retrieval', () => {
const { engine } = makeEngine()
expect(engine.evaluate(bash('pass show work/aws'))?.type).toBe('block')
expect(engine.evaluate(bash('op read op://vault/item/password'))?.type).toBe('block')
expect(engine.evaluate(bash('bw get password github'))?.type).toBe('block')
})

it('blocks decrypt commands from encryption tools', () => {
const { engine } = makeEngine()
expect(engine.evaluate(bash('gpg --decrypt secrets.gpg'))?.type).toBe('block')
expect(engine.evaluate(bash('age -d -i key.txt file.age'))?.type).toBe('block')
expect(engine.evaluate(bash('openssl enc -d -aes256 -in file.enc'))?.type).toBe('block')
})

it('force-blocks adversarial wrappers around sensitive operations', () => {
const { engine } = makeEngine()
expect(engine.evaluate(bash('eval "sops -d secrets.yaml"'))?.type).toBe('block')
expect(engine.evaluate(bash('bash -c "cat .env"'))?.type).toBe('block')
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add adversarial cases for the flagged pattern gaps.

Coverage here doesn't exercise the boundary issues raised in private-key.yaml (FIDO2 id_ed25519_sk/id_ecdsa_sk) or hardening.yaml (.keys/.environment false positives, .ssh/authorized_keys and .ssh/config over-blocking). Adding negative/positive cases for these would catch regressions in the underlying regexes.

As per path instructions for src/**/*.test.ts: "missing adversarial/edge-case coverage for matchers."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/packs/packs.test.ts` around lines 59 - 92, Add adversarial coverage in
packs.test around the matcher gaps mentioned in the review: extend the existing
makeEngine/bash/read assertions to include positive blocks for FIDO2 private
keys like id_ed25519_sk and id_ecdsa_sk, and add negative/positive cases for
hardening.yaml boundary behavior such as .keys and .environment false positives
plus .ssh/authorized_keys and .ssh/config handling. Use the existing
engine.evaluate, bash, and read tool patterns in packs.test.ts so the tests
clearly pin the regex behavior and prevent regressions.

Source: Path instructions

Comment thread src/packs/predicates.ts
import type { PredicateRegistry } from '../core/predicate-registry.js'

const NPM_INSTALL = /(^|[;&|]\s*)npm\s+(install|i|add)\b/
const GIT_COMMIT = /(^|[;&|]\s*)git\s+commit\b/

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

GIT_COMMIT regex false-positives on other git commit-* subcommands.

\bcommit\b matches "commit" followed by any non-word character, so git commit-graph write or git commit-tree also match (the - after "commit" already satisfies the trailing \b). These are unrelated plumbing/maintenance subcommands, not the git commit porcelain command the git-safety.commit-to-main rule targets, so onProtectedBranch will incorrectly trigger a confirm/block on protected branches for these commands too.

🐛 Proposed fix
-const GIT_COMMIT = /(^|[;&|]\s*)git\s+commit\b/
+const GIT_COMMIT = /(^|[;&|]\s*)git\s+commit(?:\s|$)/

Also applies to: 22-32

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/packs/predicates.ts` at line 7, The GIT_COMMIT matcher in predicates.ts
is too broad and can fire for git commit-* plumbing commands like commit-graph
or commit-tree. Tighten the pattern used by onProtectedBranch so it matches only
the porcelain git commit command, not subcommands with a hyphen after commit.
Update the GIT_COMMIT regex in the predicates logic and verify the confirm/block
path in the commit-to-main rule no longer triggers for those unrelated commands.

Comment thread src/packs/predicates.ts
Comment on lines +17 to +20
export function npmInPnpmRepo(ctx: ToolCallContext, cwd = process.cwd()): boolean {
if (!NPM_INSTALL.test(command(ctx))) return false
return existsSync(join(cwd, 'pnpm-lock.yaml'))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

npmInPnpmRepo only checks cwd, missing workspace subdirectories.

If the agent runs npm install from within a package subdirectory of a pnpm workspace (common in monorepos), pnpm-lock.yaml typically only exists at the repo root, so this predicate will false-negative and let the conflicting npm install through.

♻️ Proposed fix: walk up to find pnpm-lock.yaml
+function findUp(cwd: string, filename: string): boolean {
+  let dir = cwd
+  for (;;) {
+    if (existsSync(join(dir, filename))) return true
+    const parent = dirname(dir)
+    if (parent === dir) return false
+    dir = parent
+  }
+}
+
 export function npmInPnpmRepo(ctx: ToolCallContext, cwd = process.cwd()): boolean {
   if (!NPM_INSTALL.test(command(ctx))) return false
-  return existsSync(join(cwd, 'pnpm-lock.yaml'))
+  return findUp(cwd, 'pnpm-lock.yaml')
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function npmInPnpmRepo(ctx: ToolCallContext, cwd = process.cwd()): boolean {
if (!NPM_INSTALL.test(command(ctx))) return false
return existsSync(join(cwd, 'pnpm-lock.yaml'))
}
function findUp(cwd: string, filename: string): boolean {
let dir = cwd
for (;;) {
if (existsSync(join(dir, filename))) return true
const parent = dirname(dir)
if (parent === dir) return false
dir = parent
}
}
export function npmInPnpmRepo(ctx: ToolCallContext, cwd = process.cwd()): boolean {
if (!NPM_INSTALL.test(command(ctx))) return false
return findUp(cwd, 'pnpm-lock.yaml')
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/packs/predicates.ts` around lines 17 - 20, The npmInPnpmRepo predicate
only checks the current cwd for pnpm-lock.yaml, so it misses pnpm workspaces
when the command is run from a package subdirectory. Update npmInPnpmRepo to
walk up from cwd toward the repo root until it finds pnpm-lock.yaml (or reaches
the filesystem root), and use that result to decide whether to return true,
keeping the existing NPM_INSTALL and command(ctx) check intact.

Comment on lines +6 to +16
- id: private-key.cat
title: Block shell reads of key files
description: Catches cat/less/head/tail on .pem/.key/id_* files
phase: before-tool
match:
type: bash-command
pattern: "(^|[;&|]\\s*)(cat|less|more|head|tail)\\s+[^;&|]*(\\.pem|\\.key|id_(rsa|ed25519|ecdsa|dsa))\\b"
defaultAction:
type: block
message: 'Blocked: `{matched}` would expose private key material.'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Bash-command rule misses FIDO2/security-key filenames.

OpenSSH FIDO2-backed keys default to id_ed25519_sk / id_ecdsa_sk (underscore-separated). Since _ is a word character, \b right after ed25519/ecdsa in the current alternation won't match before the _sk suffix, so cat ~/.ssh/id_ed25519_sk slips past this rule (it's still caught by the separate .ssh file-path rule for the read tool, but not for equivalent shell access). Consider adding the _sk suffix to keep the two rules consistent in coverage.

🔧 Proposed pattern update
-      pattern: "(^|[;&|]\\s*)(cat|less|more|head|tail)\\s+[^;&|]*(\\.pem|\\.key|id_(rsa|ed25519|ecdsa|dsa))\\b"
+      pattern: "(^|[;&|]\\s*)(cat|less|more|head|tail)\\s+[^;&|]*(\\.pem|\\.key|id_(rsa|ed25519|ecdsa|dsa)(_sk)?)\\b"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- id: private-key.cat
title: Block shell reads of key files
description: Catches cat/less/head/tail on .pem/.key/id_* files
phase: before-tool
match:
type: bash-command
pattern: "(^|[;&|]\\s*)(cat|less|more|head|tail)\\s+[^;&|]*(\\.pem|\\.key|id_(rsa|ed25519|ecdsa|dsa))\\b"
defaultAction:
type: block
message: 'Blocked: `{matched}` would expose private key material.'
- id: private-key.cat
title: Block shell reads of key files
description: Catches cat/less/head/tail on .pem/.key/id_* files
phase: before-tool
match:
type: bash-command
pattern: "(^|[;&|]\\s*)(cat|less|more|head|tail)\\s+[^;&|]*(\\.pem|\\.key|id_(rsa|ed25519|ecdsa|dsa)(_sk)?)\\b"
defaultAction:
type: block
message: 'Blocked: `{matched}` would expose private key material.'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/packs/private-key.yaml` around lines 6 - 16, The bash-command matcher in
private-key.cat does not cover OpenSSH FIDO2 key filenames, so update the
pattern in the rule to also match the `id_ed25519_sk` and `id_ecdsa_sk` variants
alongside the existing `id_*` cases. Keep the change localized to the
`match.pattern` in this rule so shell reads like `cat`, `less`, `head`, and
`tail` are blocked consistently for both traditional private keys and
security-key-backed private keys.

Comment on lines +22 to +23
type: file-path
pattern: "(\\.pem|\\.key)$|/\\.ssh/(?!.*\\.pub$)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target rule pack and any nearby docs that define its intent.
git ls-files 'src/packs/private-key.yaml' 'docs/rule-pack-guide.md' 'docs/how-matching-works.md' 'docs/getting-started.md' 'docs/adrs/007-trust-and-self-protection.md'

printf '\n--- src/packs/private-key.yaml ---\n'
cat -n src/packs/private-key.yaml

printf '\n--- search for .ssh / private-key docs references ---\n'
rg -n --no-heading '\.ssh|known_hosts|authorized_keys|private key|private-key' src docs README.md

Repository: nimser/agent-guardrails

Length of output: 6462


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n--- docs/packs.md around private-key ---\n'
sed -n '68,82p' docs/packs.md

printf '\n--- src/packs/packs.test.ts around private-key tests ---\n'
sed -n '54,78p' src/packs/packs.test.ts

printf '\n--- search for any SSH-specific private-key docs/tests ---\n'
rg -n --no-heading 'known_hosts|authorized_keys|config|id_rsa|id_ed25519|\.ssh/' src docs README.md

Repository: nimser/agent-guardrails

Length of output: 5391


private-key.read matches too much under .ssh. /\.ssh/(?!.*\.pub$) also blocks known_hosts, config, and authorized_keys; narrow it if this pack is meant to protect only private-key material.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/packs/private-key.yaml` around lines 22 - 23, The private-key rule in the
private-key pack is too broad under `.ssh` because the current pattern blocks
non-secret files like `known_hosts`, `config`, and `authorized_keys`. Tighten
the pattern in the file-path matcher so it only targets actual private-key
material, and keep the existing `.pem`/`.key` coverage while narrowing the
`.ssh` path handling in the same rule.

@nimser
nimser force-pushed the feat/steering-first-direction branch from b577724 to fe159c2 Compare July 5, 2026 17:08
@nimser nimser changed the title Rebuild docs, specs, and engine around the steering-first direction Ship the first rule packs, user-input phase, and Claude Code adapter Jul 5, 2026
@nimser
nimser changed the base branch from main to feat/new-direction-alignment July 5, 2026 17:09
@nimser
nimser changed the base branch from feat/new-direction-alignment to main July 5, 2026 18:07
…adapter

Implements the three active changes on top of the direction alignment:
change-18 — nine built-in YAML packs (25 rules; steering: modern-cli,
package-manager, git-safety; security: env, sops, private-key,
secret-managers, encryption-tools, hardening) with built-in predicates
and loadBuiltInRulePacks(); change-17 — user-input phase wired through
the engine and matcher with the redactUserInput capability gate;
change-7 — Claude Code hook adapter (structured permissionDecision
JSON, updatedInput for run, native ask for confirm, UserPromptSubmit
scrubbing, unconditional fail-closed wrapper). Plus the auto-generated
docs/packs.md gallery with a CI staleness check.

Known gaps before this is user-ready (tracked in the PR body):
packaging (YAML packs into dist, yaml to dependencies), the CLI
installer, hook-schema verification against a live Claude Code, the
Pi adapter, and after-tool redact evaluation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUpa7w5hYzrXyCYkK8K1DC
@nimser
nimser force-pushed the feat/steering-first-direction branch from fe159c2 to 22bf897 Compare July 5, 2026 18:13
@sonarqubecloud

sonarqubecloud Bot commented Jul 5, 2026

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
src/engine/engine.ts (1)

51-53: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Nested ternary still unresolved.

Same nested-ternary flagged by SonarCloud in a prior review remains unfixed.

♻️ Proposed refactor
-  // user-input contexts carry prompt text, which is never command-split (ADR-010)
-  const commands =
-    ctx.toolName === 'user-input' ? [command ?? ''] : command ? splitCommands(command) : ['']
+  // user-input contexts carry prompt text, which is never command-split (ADR-010)
+  const commands = deriveCommands(ctx, command)
function deriveCommands(ctx: ToolCallContext, command: string | undefined): string[] {
  if (ctx.toolName === 'user-input') return [command ?? '']
  return command ? splitCommands(command) : ['']
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/engine/engine.ts` around lines 51 - 53, The nested ternary in the command
derivation logic is still present and should be refactored for clarity. Update
the command selection in engine.ts around the existing commands assignment by
moving the logic into a helper like deriveCommands(ctx, command) or equivalent,
using an explicit if branch for the user-input case and a separate fallback for
splitCommands(command) versus ['']. Keep the behavior identical while removing
the nested ternary from the engine.ts flow.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/generate-packs-gallery.mjs`:
- Line 11: The pack categorization in generate-packs-gallery.mjs is hardcoded
via the STEERING Set, which can silently misclassify new packs. Update the
gallery generation logic to read category from each pack’s YAML/schema data
instead of relying on the STEERING allowlist, and use that field when building
the output so new packs don’t default incorrectly. Adjust the related
pack-processing flow around STEERING and the gallery generation path to preserve
existing categories while making the source of truth explicit.

In `@src/adapters/claude-code/adapter.test.ts`:
- Around line 97-106: Add an adversarial test around toHookResponse in
adapter.test.ts that covers a redact action when the prompt contains a secret
shape not included in SECRET_SHAPES. Use the existing UserPromptSubmit/redact
flow, but assert the expected fail-closed or non-leaking behavior for a token
shape that the matcher does not currently recognize, so the test pins down the
divergence referenced by respond.ts.

In `@src/engine/engine.ts`:
- Around line 122-129: Hoist the loop-invariant effectiveCapabilities
computation out of the per-rule matching path in engine.ts so it is created once
per call instead of on every matching rule. Use the ctxPhase-dependent logic
near resolveAction and the surrounding pack.rules iteration to move the
conditional redaction capability selection before the loop, then reuse that same
HarnessCapabilities value inside the rule evaluation flow.

---

Duplicate comments:
In `@src/engine/engine.ts`:
- Around line 51-53: The nested ternary in the command derivation logic is still
present and should be refactored for clarity. Update the command selection in
engine.ts around the existing commands assignment by moving the logic into a
helper like deriveCommands(ctx, command) or equivalent, using an explicit if
branch for the user-input case and a separate fallback for
splitCommands(command) versus ['']. Keep the behavior identical while removing
the nested ternary from the engine.ts flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b5e27a67-6db6-4af6-8604-7c24bd900e7e

📥 Commits

Reviewing files that changed from the base of the PR and between b577724 and 22bf897.

📒 Files selected for processing (23)
  • .github/workflows/ci.yml
  • docs/packs.md
  • package.json
  • scripts/generate-packs-gallery.mjs
  • src/adapters/claude-code/adapter.test.ts
  • src/adapters/claude-code/hook.ts
  • src/adapters/claude-code/normalize.ts
  • src/adapters/claude-code/respond.ts
  • src/engine/engine.ts
  • src/index.ts
  • src/matcher/matchers.ts
  • src/packs/encryption-tools.yaml
  • src/packs/env.yaml
  • src/packs/git-safety.yaml
  • src/packs/hardening.yaml
  • src/packs/index.ts
  • src/packs/modern-cli.yaml
  • src/packs/package-manager.yaml
  • src/packs/packs.test.ts
  • src/packs/predicates.ts
  • src/packs/private-key.yaml
  • src/packs/secret-managers.yaml
  • src/packs/sops.yaml
📜 Review details
🧰 Additional context used
📓 Path-based instructions (7)
**

⚙️ CodeRabbit configuration file

**: # Agent Guardrails

⚠️ Heavy development: Not yet published to npm. Expect breakage. The quick start below shows the expected workflow once the package is available.


npm
Build
Coverage
Bundle Size
TypeScript
License: MIT
PRs Welcome

Quality Gate Status
Coverage

Your agent just tried cat .env. It got back the keys — not the values — and kept working.

That's the whole idea: a steering layer for AI coding agents. Instead of stopping the agent cold, rules hand it a safer or better move it can act on immediately — suggest a redacted read, run a rewritten command, redact a leaked value, confirm with a human, or block when nothing safe exists.

Batteries included

Rules work out of the box — no config, no flags:

npx agent-guardrails ...

Files:

  • src/packs/package-manager.yaml
  • src/packs/modern-cli.yaml
  • src/matcher/matchers.ts
  • package.json
  • src/packs/sops.yaml
  • src/packs/hardening.yaml
  • src/packs/index.ts
  • src/packs/git-safety.yaml
  • src/packs/secret-managers.yaml
  • src/packs/private-key.yaml
  • src/packs/encryption-tools.yaml
  • scripts/generate-packs-gallery.mjs
  • src/adapters/claude-code/normalize.ts
  • src/index.ts
  • src/packs/env.yaml
  • src/packs/packs.test.ts
  • src/adapters/claude-code/adapter.test.ts
  • docs/packs.md
  • src/engine/engine.ts
  • src/packs/predicates.ts
  • src/adapters/claude-code/respond.ts
  • src/adapters/claude-code/hook.ts
src/**/*.yaml

⚙️ CodeRabbit configuration file

YAML rule pack files. Flag if: non-kebab-case pack id; non-dotted rule id; phase/action mismatch (before-tool: block/suggest/run/confirm, after-tool: redact only); missing {matched} template in messages; suggest action without replacement.

Files:

  • src/packs/package-manager.yaml
  • src/packs/modern-cli.yaml
  • src/packs/sops.yaml
  • src/packs/hardening.yaml
  • src/packs/git-safety.yaml
  • src/packs/secret-managers.yaml
  • src/packs/private-key.yaml
  • src/packs/encryption-tools.yaml
  • src/packs/env.yaml
src/matcher/**/*.ts

⚙️ CodeRabbit configuration file

src/matcher/**/*.ts: Flag if: imports from resolver/, engine/, or infrastructure/; side effects or global state in matching logic; missing adversarial test cases for new regex patterns; reintroducing a registry/handler split.
Do NOT flag: command-splitting limitations (variable tracking is explicitly out of scope); the MatchCondition discriminated union being a closed set evaluated by a single matchesMatcher() function.

Files:

  • src/matcher/matchers.ts
src/index.ts

⚙️ CodeRabbit configuration file

src/index.ts: Flag if: default exports; new exports without corresponding documentation.
Do NOT flag: re-exports from infrastructure/. The barrel may re-export from any layer.

Files:

  • src/index.ts
src/**/*.test.ts

⚙️ CodeRabbit configuration file

Flag if: snapshot tests for regex patterns (use inline values); missing adversarial/edge-case coverage for matchers; jest imports (this project uses vitest).

Files:

  • src/packs/packs.test.ts
  • src/adapters/claude-code/adapter.test.ts
docs/**/*.md

⚙️ CodeRabbit configuration file

Flag if: broken relative links; code examples that don't match actual API signatures; incorrect vocabulary usage (see README.md vocabulary table — Behavior≠Action, RulePack≠package, Harness≠Agent).

Files:

  • docs/packs.md
src/engine/**/*.ts

⚙️ CodeRabbit configuration file

Flag if: imports from infrastructure/; orchestration growing beyond ~80 lines (business logic belongs in resolver/ or matcher/); non-fail-closed handling of malformed ToolCallContext.

Files:

  • src/engine/engine.ts
🪛 GitHub Check: SonarCloud Code Analysis
src/engine/engine.ts

[warning] 53-53: Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=nimser_agent-guardrails&issues=AZ8zJbg-pHPotZ9rhgby&open=AZ8zJbg-pHPotZ9rhgby&pullRequest=15

src/adapters/claude-code/respond.ts

[warning] 97-97: Use concise character class syntax '\w' instead of '[A-Za-z0-9_]'.

See more on https://sonarcloud.io/project/issues?id=nimser_agent-guardrails&issues=AZ8zJbjppHPotZ9rhgbz&open=AZ8zJbjppHPotZ9rhgbz&pullRequest=15

🪛 zizmor (1.26.1)
.github/workflows/ci.yml

[warning] 10-26: overly broad permissions (excessive-permissions): default permissions used due to no permissions: block

(excessive-permissions)

🔇 Additional comments (31)
src/engine/engine.ts (2)

104-109: LGTM!


148-165: LGTM!

src/matcher/matchers.ts (1)

34-43: LGTM!

src/index.ts (1)

24-33: LGTM!

Also applies to: 55-58

src/adapters/claude-code/hook.ts (1)

47-55: main() startup path still not fail-closed and stdin read is unbounded.

createClaudeCodeEngine() (pack loading) and the for await ... process.stdin loop run outside any try/catch, so a startup crash exits without emitting a block decision; the read also waits indefinitely for EOF. Both were raised on prior commits and remain unaddressed.

src/adapters/claude-code/normalize.ts (1)

30-36: 🩺 Stability & Availability

Missing filePath is handled safely. matchesMatcher() returns false when filePath is absent, and isMissingRequiredFields() blocks read/write calls without one, so this path won’t dereference undefined.

			> Likely an incorrect or invalid review comment.
src/adapters/claude-code/respond.ts (1)

73-85: 🔒 Security & Privacy

No leak path here. The only user-input redact rule matches the same secret-shaped tokens this adapter scrubs, so this path doesn’t currently let the raw prompt through.

			> Likely an incorrect or invalid review comment.
src/packs/index.ts (2)

9-18: 🩺 Stability & Availability

Ship YAML packs alongside compiled output.

BUILTIN_PACKS_DIR still resolves next to the compiled JS, but no build/publish step copies src/packs/*.yaml into dist/packs. loadBuiltInRulePacks() will return an empty list once installed from npm.


1-6: LGTM!

Also applies to: 20-20

src/packs/predicates.ts (3)

7-7: 🎯 Functional Correctness

GIT_COMMIT still matches unrelated git commit-* plumbing subcommands.


17-20: 🎯 Functional Correctness

npmInPnpmRepo still only checks cwd, missing pnpm workspace subdirectories.


1-6: LGTM!

Also applies to: 34-55

src/packs/modern-cli.yaml (3)

6-16: 🎯 Functional Correctness

grep pattern still misses uppercase -R.


18-28: 🎯 Functional Correctness

find pattern still misses the path-omitted form (find -name ...).


30-40: LGTM!

src/packs/git-safety.yaml (3)

18-31: 🎯 Functional Correctness

commit-to-main messages still missing {matched}.


6-16: LGTM!


32-44: 🎯 Functional Correctness | ⚡ Quick win

git clean --force (long flag) bypasses the confirm gate.

The clean alternative -[a-zA-Z]*f only matches short-flag clusters ending in f (-f, -df, -xdf, …). git clean --force — a valid, common invocation — doesn't match: after the leading -, the next character is -, which fails [a-zA-Z]*, so the destructive clean never trips the confirm gate.

🐛 Proposed fix
-      pattern: "(^|[;&|]\\s*)git\\s+(reset\\s+[^;&|]*--hard|clean\\s+[^;&|]*-[a-zA-Z]*f)"
+      pattern: "(^|[;&|]\\s*)git\\s+(reset\\s+[^;&|]*--hard|clean\\s+[^;&|]*(-[a-zA-Z]*f\\b|--force\\b))"
src/packs/private-key.yaml (2)

6-16: 🎯 Functional Correctness

Bash-command rule still misses FIDO2/security-key filenames (_sk suffix).


17-27: 🎯 Functional Correctness

private-key.read still over-broadly blocks all of .ssh/ (e.g. known_hosts, config, authorized_keys).

src/packs/package-manager.yaml (1)

1-17: LGTM!

src/packs/secret-managers.yaml (2)

6-16: 🔒 Security & Privacy | ⚡ Quick win

pass grep bypasses this block rule.

pass grep <term> decrypts every password file and prints matching lines — functionally equivalent to pass show for secret exposure — but isn't matched by show\b|\S*/.

🐛 Proposed fix
-      pattern: "(^|[;&|]\\s*)pass\\s+(show\\b|\\S*/)"
+      pattern: "(^|[;&|]\\s*)pass\\s+(show\\b|grep\\b|\\S*/)"

17-49: LGTM!

src/packs/encryption-tools.yaml (1)

1-38: LGTM!

src/packs/hardening.yaml (1)

6-37: 🎯 Functional Correctness

Wrapper patterns still over-match on unrelated filenames.

hardening.eval/shell-c/substitution (Lines 12, 23, 34) match raw substrings .env/.pem/.key without word boundaries, so notes.keys, backup.keychain, app.environment still get force-blocked. The sops branch is also still inconsistent (sops\b implicit vs sops\s in substitution).

src/packs/packs.test.ts (1)

59-92: 📐 Maintainability & Code Quality

Adversarial/edge-case coverage still missing for flagged pattern gaps.

No tests exercise the FIDO2 key boundary (id_ed25519_sk/id_ecdsa_sk) or hardening.yaml's .keys/.environment/.ssh/authorized_keys/.ssh/config false-positive risks.

Source: Path instructions

.github/workflows/ci.yml (1)

25-26: 🔒 Security & Privacy

check job still runs with default unscoped GITHUB_TOKEN permissions.

The new gallery-verification step doesn't need write access either; add an explicit least-privilege permissions: block at the job level.

Source: Linters/SAST tools

src/packs/env.yaml (1)

1-51: LGTM!

src/packs/sops.yaml (1)

1-28: LGTM!

package.json (1)

43-44: LGTM!

docs/packs.md (1)

1-98: LGTM!

const root = join(dirname(fileURLToPath(import.meta.url)), '..')
const packsDir = join(root, 'src', 'packs')

const STEERING = new Set(['modern-cli', 'package-manager', 'git-safety'])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Categorization is a hardcoded id list, prone to silent drift.

New packs must be manually added to STEERING (Line 11) or they default to "Security" — miscategorization wouldn't be caught by the CI staleness check since the generated output would still be internally consistent. Consider deriving the category from a category field in the pack YAML/schema instead.

Also applies to: 34-35

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/generate-packs-gallery.mjs` at line 11, The pack categorization in
generate-packs-gallery.mjs is hardcoded via the STEERING Set, which can silently
misclassify new packs. Update the gallery generation logic to read category from
each pack’s YAML/schema data instead of relying on the STEERING allowlist, and
use that field when building the output so new packs don’t default incorrectly.
Adjust the related pack-processing flow around STEERING and the gallery
generation path to preserve existing categories while making the source of truth
explicit.

Comment on lines +97 to +106
it('rewrites the prompt for user-input redact', () => {
const r = toHookResponse(
{ type: 'redact', replacement: '[SECRET-REDACTED]' },
{
hook_event_name: 'UserPromptSubmit',
prompt: 'my key is ghp_abcdefghijklmnopqrstuvwxyz0123456789 ok',
}
)
expect(r.hookSpecificOutput?.updatedPrompt).toBe('my key is [SECRET-REDACTED] ok')
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add adversarial coverage for redact vs. SECRET_SHAPES divergence.

The redact test only exercises a ghp_ token that SECRET_SHAPES recognizes. Add a case where the engine returns redact but the prompt holds a secret shape not in SECRET_SHAPES to pin down the leak/fail-closed behavior (see the related concern in respond.ts).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/adapters/claude-code/adapter.test.ts` around lines 97 - 106, Add an
adversarial test around toHookResponse in adapter.test.ts that covers a redact
action when the prompt contains a secret shape not included in SECRET_SHAPES.
Use the existing UserPromptSubmit/redact flow, but assert the expected
fail-closed or non-leaking behavior for a token shape that the matcher does not
currently recognize, so the test pins down the divergence referenced by
respond.ts.

Comment thread src/engine/engine.ts
Comment on lines +122 to +129
// In the user-input phase, prompt rewriting is gated on the dedicated
// redactUserInput capability rather than tool-output redact (ADR-010).
const effectiveCapabilities: HarnessCapabilities =
ctxPhase === 'user-input'
? { ...capabilities, redact: capabilities.redactUserInput === true }
: capabilities

const resolved = resolveAction(rule.defaultAction, effectiveCapabilities, {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Hoist effectiveCapabilities out of the per-rule loop.

ctxPhase is loop-invariant, so effectiveCapabilities can be computed once before iterating pack.rules instead of being recreated on every matching rule.

♻️ Proposed refactor
 function matchPackRulesTraced(
   pack: RulePack,
   matchCtx: ToolCallContext,
   capabilities: HarnessCapabilities,
   ctx: ToolCallContext,
   cmd: string,
   registry: PredicateRegistry
 ): MatchResult | undefined {
   const ctxPhase = ctx.toolName === 'user-input' ? 'user-input' : 'before-tool'
+  const effectiveCapabilities: HarnessCapabilities =
+    ctxPhase === 'user-input'
+      ? { ...capabilities, redact: capabilities.redactUserInput === true }
+      : capabilities
   for (const rule of pack.rules) {
     ...
     if (rule.phase !== ctxPhase) continue
     if (!matchesMatcher(rule.match, matchCtx, registry)) continue
     ...
-    // In the user-input phase, prompt rewriting is gated on the dedicated
-    // redactUserInput capability rather than tool-output redact (ADR-010).
-    const effectiveCapabilities: HarnessCapabilities =
-      ctxPhase === 'user-input'
-        ? { ...capabilities, redact: capabilities.redactUserInput === true }
-        : capabilities
-
     const resolved = resolveAction(rule.defaultAction, effectiveCapabilities, {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// In the user-input phase, prompt rewriting is gated on the dedicated
// redactUserInput capability rather than tool-output redact (ADR-010).
const effectiveCapabilities: HarnessCapabilities =
ctxPhase === 'user-input'
? { ...capabilities, redact: capabilities.redactUserInput === true }
: capabilities
const resolved = resolveAction(rule.defaultAction, effectiveCapabilities, {
const effectiveCapabilities: HarnessCapabilities =
ctxPhase === 'user-input'
? { ...capabilities, redact: capabilities.redactUserInput === true }
: capabilities
const resolved = resolveAction(rule.defaultAction, effectiveCapabilities, {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/engine/engine.ts` around lines 122 - 129, Hoist the loop-invariant
effectiveCapabilities computation out of the per-rule matching path in engine.ts
so it is created once per call instead of on every matching rule. Use the
ctxPhase-dependent logic near resolveAction and the surrounding pack.rules
iteration to move the conditional redaction capability selection before the
loop, then reuse that same HarnessCapabilities value inside the rule evaluation
flow.

@nimser nimser changed the title Ship the first rule packs, user-input phase, and Claude Code adapter feat: steering-first surface — change-17 user-input phase, change-18 QoL steering packs, change-7 claude-code adapter Jul 5, 2026
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