Skip to content
Download PDF

The Guardrails Gate ​

The guardrails gate is one of Forge's four enforced controls. It bounds dangerous actions — the irreversible command, the leaked secret — the failure class an advisory paragraph cannot stop. It is the one control that stands in front of an action instead of only grading a run after the fact.

Two lines of defense ​

Guardrails are layered, because Forge is not in the model's inference loop — it cannot intercept a keystroke. So it does both jobs it can do:

  1. Feedforward, before an action runs. A secrets deny-list the agent's own permission system enforces, and a destructive-action PreToolUse hook (forge guardrails-gate) that blocks a catastrophic command with a non-zero exit.
  2. Enforced scan, after files are written. forge secscan reads the working tree for leaked secrets, PII, and prompt-injection strings, and returns non-zero while any unwaived finding remains.

The first line steers; the second refuses to bless.

Guide and Sensor in one gate

In the harness model, the deny-list and the destructive-action hook are Guides — feedforward, they shape the run before it acts. forge secscan is a Sensor — feedback, it reads what actually happened and refuses. The guardrails gate is the one control that wears both hats.

The secrets deny-list ​

When Forge configures an agent, it writes a per-adapter permission overlay that denies both Read and Edit on the files that must never reach a model's context or its edits — dotenv files (.env, .env.*), private keys and certificates (*.pem, *.key), SSH and cloud credentials (.ssh/**, .aws/credentials), OS keychains, and anything under a secrets/ directory. The same categories are denied across every adapter, so switching agents never quietly widens what is readable.

The deny-list is enforced by the agent's own permission system, not by Forge. Forge's job is to write a correct, consistent overlay into every adapter it manages; coverage then depends on what each host supports — see per-adapter coverage.

The destructive-action hook ​

For Claude Code running in bypass (auto-accept) mode — where the agent runs every tool call without asking — Forge installs a PreToolUse hook wired to forge guardrails-gate. The host hands Forge the command on stdin; Forge evaluates it and returns the exit code the host obeys:

  • Exit 2 — blocked. The command matches a catastrophic pattern. The host refuses to run it and relays Forge's reason back to the model.
  • Exit 0 — allowed. Everything else proceeds.

What it denies ​

The gate blocks the small set of commands that are irreversible or history-destroying: a recursive-and-forced rm whose target is the filesystem root or home directory (rm -rf /, rm -rf ~), any sudo-elevated rm, git reset --hard, git rebase, and a hard force-push (git push --force / -f). It deliberately allows --force-with-lease and --force-if-includes — the safe force-push variants that refuse to clobber unseen remote work.

Matching runs over the whole command string, split into statements on ;, &, |, and newlines, with simple subshells unwrapped — so a destructive command chained after a harmless one is still caught.

What "ask" covers ​

Under guarded mode the agent already pauses for confirmation on every tool call, so the blocking hook is not installed. Instead the per-adapter overlay marks the outward-facing commands — a bare git commit, git push, an arbitrary rm -rf <path>, piped shell installers, recursive chmod/chown — as ask, and the human confirms. The blocking gate exists precisely for bypass mode, where there is no "ask" to fall back on.

The gate is fail-open by design

If the hook payload is unreadable, malformed, or the tool is not Bash, forge guardrails-gate returns exit 0 and allows the call. Only a confirmed destructive match blocks. This is deliberate: a narrow safety gate must never become the thing that disables Bash entirely because a host changed its payload shape.

forge secscan — the enforced scan ​

The deny-list and the hook stop shapes Forge can see coming. forge secscan catches what already landed. It statically scans a change's files for leaked secrets, PII, and prompt-injection strings, and enforces a gate over the findings.

sh
forge secscan scan  --change <c> [--path P ...] [--min-severity high|medium]
forge secscan waive --change <c> --fingerprint <fp>
forge secscan reset --change <c>
  • scan (the default) reads the change's files and evaluates the gate. It is read-only over the tree — it only reads the waivers file, never writes it. A finding blocks (non-zero exit) when it is unwaived and at or above the severity floor. Secrets and prompt-injection are high; PII is medium. The default floor is high, so PII is reported but does not block unless you lower the floor.
  • waive records a human-acknowledged fingerprint so it stops blocking — the only way a finding is accepted. Waivers persist in .forge/changes/<c>/secscan.yaml.
  • reset clears every waiver for the change.
text
$ forge secscan scan --change checkout
secscan[checkout] high secret src/config.ts:14 fp=a1b2c3 aws-access-key AKIA…
SECSCAN BLOCKED — 1 unwaived finding(s). STOP and escalate to a human.
# exit 1

$ forge secscan waive --change checkout --fingerprint a1b2c3   # a human accepts it
secscan waived a1b2c3 for change "checkout"
# exit 0

$ forge secscan scan --change checkout
secscan: clean (1 finding(s), all waived or below threshold)
# exit 0

Each finding prints one line — secscan[<c>] <severity> <kind> <file>:<line> fp=<fingerprint> <redacted> — and the redacted snippet never contains the raw secret.

A human waives, never the agent

secscan returns non-zero for a reason: an unwaived finding is a stop-and-escalate signal. The agent must not waive its own findings — only a human, after confirming a false positive or an accepted risk, runs forge secscan waive. The gate is idempotent, so it re-runs to the same verdict from a hook, a script, or by hand. The /forge.secscan slash command shells out to the same binary, so agent-side and terminal behavior are identical.

The honest limit ​

Forge is not in the model's inference loop. It cannot un-write a secret or intercept a keystroke — secscan scans the working tree after the file was saved, and the destructive-action hook only sees what the host chooses to hand it. Both are best-effort, not a sandbox and not a policy engine:

  • The destructive-action gate is a heuristic textual matcher, not a shell parser. It catches the shapes the team has encoded; it does not defend against deliberate obfuscation — base64 payloads, eval, environment-variable indirection.
  • Coverage is bounded by what each host exposes.

Treat guardrails as a second line of defense behind reviewing diffs and confirming ask prompts yourself — not a substitute for either.

Per-adapter coverage ​

Only some hosts expose an injectable blocking hook. Forge writes the strongest guardrail each adapter supports and documents the gaps rather than emitting speculative config:

AdapterGuardrail mechanism
Claude CodeRead/Edit + catastrophic-rm deny-list; forge guardrails-gate PreToolUse hook under bypass
OpenCoderead deny-list + bash ask-overlay (commit / push / rebase / reset --hard)
Gemini CLI / Qwen Codemode-level approval (default guarded, auto_edit bypass)
Codexapproval_policy toggle + deny-list; no per-command blocking hook
VS Code Copilotglobal auto-approve toggle; no per-command gate

forge secscan is host-independent — it runs against the working tree from a terminal, a hook, or CI, regardless of adapter.

Next steps ​