# The Guardrails Gate

The guardrails gate is one of Forge's four [enforced controls](/harness/overview). 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.

::: info Guide and Sensor in one gate
In [the harness model](/harness/overview), 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](#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.

```mermaid
flowchart TD
    A["Agent about to run a Bash command"] --> B["PreToolUse hook calls forge guardrails-gate"]
    B --> C{"Catastrophic pattern?"}
    C -->|"rm -rf / · sudo rm · git reset --hard · git rebase · force-push"| D["exit 2 — blocked, reason relayed to model"]
    C -->|Anything else| E["exit 0 — allowed"]
```

### 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.

::: warning 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.

::: warning 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:

| Adapter | Guardrail mechanism |
| --- | --- |
| Claude Code | Read/Edit + catastrophic-`rm` deny-list; `forge guardrails-gate` PreToolUse hook under bypass |
| OpenCode | `read` deny-list + `bash` ask-overlay (commit / push / rebase / reset --hard) |
| Gemini CLI / Qwen Code | mode-level approval (`default` guarded, `auto_edit` bypass) |
| Codex | `approval_policy` toggle + deny-list; no per-command blocking hook |
| VS Code Copilot | global 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

- [The enforcement doctrine](/harness/overview) — where guardrails sit among the four controls.
- [Verify circuit-breaker](/harness/circuit-breaker) — the sibling gate that bounds iterations.
- [Spend budget](/harness/budget) — the sibling gate that bounds cost.
- [Connect a live agent](/harness/live-agent-hooks) — wiring the PreToolUse and scan hooks that call Forge.
- [Change governance](/features/governance) — the advisory triage contract, distinct from these gates.
