@agenttrail/guardrails 0.2.1 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +325 -122
- package/dist/{chunk-SLWHBH7H.js → chunk-6YROGDEH.js} +103 -29
- package/dist/chunk-6YROGDEH.js.map +1 -0
- package/dist/chunk-T6MJIV52.js +72 -0
- package/dist/chunk-T6MJIV52.js.map +1 -0
- package/dist/{chunk-FTTMZCDH.js → chunk-U7KZ447W.js} +65 -4
- package/dist/chunk-U7KZ447W.js.map +1 -0
- package/dist/guardrails.cjs +171 -28
- package/dist/guardrails.cjs.map +1 -1
- package/dist/guardrails.d.cts +60 -65
- package/dist/guardrails.d.ts +60 -65
- package/dist/guardrails.js +17 -1
- package/dist/index.cjs +230 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +20 -2
- package/dist/schema-B2pYoi9Z.d.cts +951 -0
- package/dist/schema-B2pYoi9Z.d.ts +951 -0
- package/dist/schema.cjs +91 -3
- package/dist/schema.cjs.map +1 -1
- package/dist/schema.d.cts +2 -892
- package/dist/schema.d.ts +2 -892
- package/dist/schema.js +4 -1
- package/package.json +30 -38
- package/dist/chunk-FTTMZCDH.js.map +0 -1
- package/dist/chunk-SLWHBH7H.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,50 +1,122 @@
|
|
|
1
|
-
<!-- cspell:words exfiltration kubeconfig -->
|
|
1
|
+
<!-- cspell:words exfiltration kubeconfig hostnames agenttrailhq OWASP slopsquatted -->
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<div align="center">
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
<a href="https://www.agenttrail.sh">
|
|
6
|
+
<picture>
|
|
7
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://www.agenttrail.sh/brand/agenttrail_logo_white.svg" />
|
|
8
|
+
<img src="https://www.agenttrail.sh/brand/agenttrail_logo.svg" alt="AgentTrail" width="220" />
|
|
9
|
+
</picture>
|
|
10
|
+
</a>
|
|
6
11
|
|
|
7
|
-
|
|
12
|
+
# AgentTrail Guardrails: an open-source guardrail library for AI coding agents
|
|
8
13
|
|
|
9
|
-
|
|
14
|
+
**A library of guardrails for catching dangerous commands before an AI coding agent runs them.**<br />
|
|
15
|
+
Plain data you can read, test, and change. Enforced in **Claude Code**, **Cursor**, and **Codex CLI**.
|
|
16
|
+
|
|
17
|
+
[](https://github.com/agenttrailhq/guardrails/actions/workflows/ci.yml)
|
|
18
|
+
[](https://www.npmjs.com/package/@agenttrail/guardrails)
|
|
19
|
+
[](LICENSE)
|
|
20
|
+
[](https://nodejs.org/)
|
|
21
|
+
|
|
22
|
+
[Quick start](#quick-start) ·
|
|
23
|
+
[The packs](#the-eleven-packs) ·
|
|
24
|
+
[OWASP coverage](#owasp-coverage) ·
|
|
25
|
+
[Guardrail format](#a-guardrail-start-to-finish) ·
|
|
26
|
+
[Contribute a guardrail](#contributing-a-guardrail) ·
|
|
27
|
+
[Changelog](CHANGELOG.md)
|
|
28
|
+
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
[74 guardrails](https://www.agenttrail.sh/guardrails), grouped into 11 packs. Apache-2.0. Published
|
|
32
|
+
as `@agenttrail/guardrails` and enforced by [AgentTrail Guard](https://github.com/agenttrailhq/guard).
|
|
33
|
+
|
|
34
|
+

|
|
35
|
+
|
|
36
|
+
<sub>A real Claude Code session. The agent decides to reset the database to get a schema push
|
|
37
|
+
through; Guard blocks `prisma db push --force-reset` with `dd.accept-data-loss` before it runs.</sub>
|
|
10
38
|
|
|
11
39
|
## What this is, in plain terms
|
|
12
40
|
|
|
13
|
-
|
|
14
|
-
not have typed yourself
|
|
41
|
+
Your AI coding agent proposes commands in your terminal all day, and some are ones you would
|
|
42
|
+
not have typed yourself: `git reset --hard` over a day's work, `rm -rf` on the wrong path, a
|
|
15
43
|
`terraform apply` against production.
|
|
16
44
|
|
|
17
45
|
This package is the **list of things worth stopping**, written as data. Each entry says what to look
|
|
18
|
-
for, how serious it is, and what should happen
|
|
46
|
+
for, how serious it is, and what should happen: allow it, ask a human first, or refuse.
|
|
19
47
|
|
|
20
48
|
**That is all this package does.** It contains no code that watches your machine and nothing that
|
|
21
|
-
talks to the network.
|
|
49
|
+
talks to the network. Something else has to read the list and act on it: normally
|
|
22
50
|
[`agenttrail-guard`](https://github.com/agenttrailhq/guard), which runs on your laptop and checks each
|
|
23
|
-
command an agent proposes against these
|
|
51
|
+
command an agent proposes against these guardrails.
|
|
52
|
+
|
|
53
|
+
## Why these guardrails?
|
|
24
54
|
|
|
25
|
-
|
|
26
|
-
|
|
55
|
+
- **Readable.** Every guardrail is plain data with a title, a severity, a default action, and a
|
|
56
|
+
description that says what it catches **and what it misses**.
|
|
57
|
+
- **Proven in both directions.** Every guardrail ships commands it must catch and near-misses it must
|
|
58
|
+
leave alone, and a shared corpus of everyday commands no guardrail may match. A guardrail that fails
|
|
59
|
+
either side does not build.
|
|
60
|
+
- **Quiet on purpose.** A commit message or a `grep` that only *mentions* `rm -rf /` is not
|
|
61
|
+
treated as running it. See [Talking about a command is not running it](#talking-about-a-command-is-not-running-it).
|
|
62
|
+
- **Mapped to known risks.** The packs line up with the
|
|
63
|
+
[OWASP Top 10s for agentic and LLM applications](#owasp-coverage), and the gaps are stated, not
|
|
64
|
+
hidden.
|
|
65
|
+
- **Separate from the enforcer.** You can read, disagree with, and change any guardrail without
|
|
66
|
+
trusting anything about the tool that enforces it.
|
|
27
67
|
|
|
28
|
-
##
|
|
68
|
+
## Quick start
|
|
29
69
|
|
|
30
|
-
|
|
31
|
-
your own from scratch.
|
|
32
|
-
- **You want to see exactly what is being blocked and why.** Every rule is plain data with a written
|
|
33
|
-
explanation, including what it *misses*.
|
|
34
|
-
- **You want to contribute a rule** you wish had existed. See [Contributing](#contributing-a-rule).
|
|
70
|
+
**You need:** Node.js 22.13 or newer.
|
|
35
71
|
|
|
36
|
-
|
|
72
|
+
### Enforce the guardrails in your agent
|
|
73
|
+
|
|
74
|
+
Most people never install this package directly. [AgentTrail Guard](https://github.com/agenttrailhq/guard)
|
|
75
|
+
bundles it, so installing Guard is all it takes:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npm install -g @agenttrail/guard
|
|
79
|
+
agenttrail-guard init --agent claude # or: --agent cursor, --agent codex
|
|
80
|
+
agenttrail-guard guardrails list # every guardrail and its current action
|
|
81
|
+
agenttrail-guard guardrails show wt.reset-hard # one guardrail in full
|
|
82
|
+
agenttrail-guard guardrails set-action <id> warn # change what a guardrail does
|
|
83
|
+
agenttrail-guard guardrails allow <id> <pattern> # silence one guardrail for one command shape
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
See the [AgentTrail Guard README](https://github.com/agenttrailhq/guard#quick-start) for setup
|
|
87
|
+
and everything else Guard can do.
|
|
88
|
+
|
|
89
|
+
### Use the guardrails in your own code
|
|
90
|
+
|
|
91
|
+
Install the package if you are writing guardrails, or building your own tool on top of the list:
|
|
37
92
|
|
|
38
93
|
```bash
|
|
39
94
|
npm install @agenttrail/guardrails
|
|
40
95
|
```
|
|
41
96
|
|
|
42
|
-
|
|
43
|
-
|
|
97
|
+
```ts
|
|
98
|
+
import { RULES, getRule, rulesForPack } from "@agenttrail/guardrails/guardrails";
|
|
99
|
+
|
|
100
|
+
// Everything that refuses outright, rather than asking or warning.
|
|
101
|
+
const blocking = RULES.filter((rule) => rule.defaultAction === "block");
|
|
102
|
+
console.log(`${RULES.length} guardrails, ${blocking.length} block by default`);
|
|
103
|
+
|
|
104
|
+
// One pack, with each guardrail's severity and default action.
|
|
105
|
+
for (const rule of rulesForPack("secret-exposure")) {
|
|
106
|
+
console.log(rule.id, rule.severity, rule.defaultAction);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// One guardrail, including the limits its description admits to.
|
|
110
|
+
console.log(getRule("wt.reset-hard")?.description);
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
This package has no evaluator: it tells you what each guardrail looks for, not whether a given
|
|
114
|
+
command matches. Guard does the matching. [Two ways to import it](#two-ways-to-import-it)
|
|
115
|
+
explains the two entry points.
|
|
44
116
|
|
|
45
|
-
## A
|
|
117
|
+
## A guardrail, start to finish
|
|
46
118
|
|
|
47
|
-
Here is a complete
|
|
119
|
+
Here is a complete guardrail. Nothing is hidden; this is the actual shape.
|
|
48
120
|
|
|
49
121
|
```jsonc
|
|
50
122
|
{
|
|
@@ -53,7 +125,8 @@ Here is a complete rule. Nothing is hidden; this is the actual shape.
|
|
|
53
125
|
"severity": "high",
|
|
54
126
|
"defaultAction": "block",
|
|
55
127
|
"title": "git reset --hard discards uncommitted work",
|
|
56
|
-
"description": "Discards all uncommitted changes. Does not match `git restore`
|
|
128
|
+
"description": "Discards all uncommitted changes. Does not match `git restore` (see wt.restore-path).",
|
|
129
|
+
"owasp": ["ASI02", "LLM03"],
|
|
57
130
|
|
|
58
131
|
// What to look for. This one matches a Bash command against a regular expression.
|
|
59
132
|
"match": {
|
|
@@ -62,7 +135,7 @@ Here is a complete rule. Nothing is hidden; this is the actual shape.
|
|
|
62
135
|
]
|
|
63
136
|
},
|
|
64
137
|
|
|
65
|
-
// Proof it works, in both directions
|
|
138
|
+
// Proof it works, in both directions (see "Every guardrail proves both directions" below).
|
|
66
139
|
"fixtures": {
|
|
67
140
|
"block": ["git reset --hard"],
|
|
68
141
|
"allow": ["git reset src/api.ts"]
|
|
@@ -70,48 +143,103 @@ Here is a complete rule. Nothing is hidden; this is the actual shape.
|
|
|
70
143
|
}
|
|
71
144
|
```
|
|
72
145
|
|
|
146
|
+
The real guardrail is in [`wt-reset-hard.ts`](src/packs/working-tree/wt-reset-hard.ts); this is a
|
|
147
|
+
trimmed copy. With it enforced by Guard, an agent that proposes `git reset --hard` is refused
|
|
148
|
+
and told which guardrail stopped it, while `git reset src/api.ts`, which unstages one file, goes
|
|
149
|
+
ahead as normal.
|
|
150
|
+
|
|
73
151
|
Reading the fields:
|
|
74
152
|
|
|
75
153
|
| Field | What it means |
|
|
76
154
|
|---|---|
|
|
77
|
-
| `id` | A stable name. Users type it to disable or change a
|
|
78
|
-
| `severity` | How bad the thing being caught is: `critical`, `high`, `medium`, `low`, `info`. **It is not a price
|
|
155
|
+
| `id` | A stable name. Users type it to disable or change a guardrail, so it never changes. |
|
|
156
|
+
| `severity` | How bad the thing being caught is: `critical`, `high`, `medium`, `low`, `info`. **It is not a price**: this package ships no mapping from severity to money. |
|
|
79
157
|
| `defaultAction` | What should happen: `block` (refuse), `require_approval` (ask a human), or `warn` (allow, but say so). A user can override it. |
|
|
80
|
-
| `description` | What the
|
|
158
|
+
| `description` | What the guardrail catches **and what it misses**. The honest limits are part of the guardrail, not a footnote. |
|
|
159
|
+
| `owasp` | Optional. The [OWASP risks](#owasp-coverage) the guardrail addresses, as ids such as `ASI02` or `LLM06`, in list order. Every guardrail in this package has at least one. Versions up to and including 0.2.2 reject the field, so validate a guardrail that carries it with 0.2.3 or later. |
|
|
81
160
|
| `match` | The condition. `any_of` means "any one of these is enough". |
|
|
82
161
|
| `fixtures` | Examples that must match, and examples that must not. |
|
|
83
162
|
|
|
84
163
|
## The eleven packs
|
|
85
164
|
|
|
86
|
-
A
|
|
165
|
+
A guardrail is filed by **the harm it prevents**, never by the technique it uses to spot it.
|
|
87
166
|
|
|
88
|
-
That sounds like a detail and is not. The three
|
|
89
|
-
endpoints all work by matching file paths
|
|
167
|
+
That sounds like a detail and is not. The three guardrails about production config, `.env` files and API
|
|
168
|
+
endpoints all work by matching file paths, but they are *not* in `file-scope`. Someone who turned
|
|
90
169
|
that pack off to stop path noise would otherwise silently lose their production and secret
|
|
91
170
|
protection, which they never asked to turn off and would not know they had.
|
|
92
171
|
|
|
93
|
-
| Pack |
|
|
172
|
+
| Pack | Guardrails | What it is about |
|
|
94
173
|
|---|---:|---|
|
|
95
|
-
| `working-tree` | 9 | Destroying uncommitted work or published history
|
|
96
|
-
| `destructive-data` | 8 | Data git cannot bring back
|
|
97
|
-
| `prod-infra` | 8 | Changing running infrastructure
|
|
98
|
-
| `secret-exposure` | 10 | Credentials and sensitive data leaving where they live. Mostly `warn`: reading a secret is a normal part of a normal day. |
|
|
99
|
-
| `rce-supply-chain` | 6 | Running code nobody reviewed
|
|
100
|
-
| `safety-bypass` | 7 | Turning off a check somebody installed on purpose, or erasing the record of it
|
|
101
|
-
| `privilege-supply-chain` | 6 | Gaining reach or handing it out
|
|
102
|
-
| `file-scope` | 4 | The agent wrote somewhere it had no business writing
|
|
103
|
-
| `agent-context` | 6 | The agent changing what it is or what it knows
|
|
104
|
-
| `test-integrity` | 6 | The agent making its work look successful
|
|
105
|
-
| `exfiltration` | 4 | Moving data off the machine or opening a way in
|
|
174
|
+
| [`working-tree`](src/packs/working-tree/) | 9 | Destroying uncommitted work or published history: `git reset --hard`, `git clean -fd`, force-push, `rm -rf`. |
|
|
175
|
+
| [`destructive-data`](src/packs/destructive-data/) | 8 | Data git cannot bring back: a dropped volume, a dropped database, destructive DDL, a deleted shadow copy. |
|
|
176
|
+
| [`prod-infra`](src/packs/prod-infra/) | 8 | Changing running infrastructure: Terraform, Kubernetes, Helm, cloud deletes, a deploy that names production. |
|
|
177
|
+
| [`secret-exposure`](src/packs/secret-exposure/) | 10 | Credentials and sensitive data leaving where they live. Mostly `warn`: reading a secret is a normal part of a normal day. |
|
|
178
|
+
| [`rce-supply-chain`](src/packs/rce-supply-chain/) | 6 | Running code nobody reviewed: pipe-to-shell, a remote runner, a redirected registry, TLS verification off. |
|
|
179
|
+
| [`safety-bypass`](src/packs/safety-bypass/) | 7 | Turning off a check somebody installed on purpose, or erasing the record of it: `--no-verify`, `--admin` merge, hooks disabled, host-key checking off, history and log purges, forged terminal output. |
|
|
180
|
+
| [`privilege-supply-chain`](src/packs/privilege-supply-chain/) | 6 | Gaining reach or handing it out: `sudo` writes, `chmod 777`, IAM grants, persistence, publishing, new dependencies. |
|
|
181
|
+
| [`file-scope`](src/packs/file-scope/) | 4 | The agent wrote somewhere it had no business writing: its own config, the machine, git's internals, the CI definition. |
|
|
182
|
+
| [`agent-context`](src/packs/agent-context/) | 6 | The agent changing what it is or what it knows (its standing instructions, its memory, its skills and commands, its MCP servers), or starting more agents, or switching another agent's approvals off. |
|
|
183
|
+
| [`test-integrity`](src/packs/test-integrity/) | 6 | The agent making its work look successful: deleting a test, weakening a runner's configuration, accepting every snapshot, switching a coverage gate off, silencing failures in bulk, or telling CI not to run. |
|
|
184
|
+
| [`exfiltration`](src/packs/exfiltration/) | 4 | Moving data off the machine or opening a way in: a reverse shell, a public tunnel, a file upload, a paste service. Command channel only. |
|
|
106
185
|
|
|
107
186
|
Pack names appear in user config files, so renaming one is a breaking change, not a tidy-up.
|
|
108
187
|
|
|
188
|
+
Each pack name links to its source folder: one TypeScript file per guardrail, so you can read any guardrail
|
|
189
|
+
end to end. Every guardrail, with its full description and tested examples, is also in the
|
|
190
|
+
[guardrail library](https://www.agenttrail.sh/guardrails) on the AgentTrail website, where you
|
|
191
|
+
can paste a command to see which guardrails match it.
|
|
192
|
+
|
|
193
|
+
## OWASP coverage
|
|
194
|
+
|
|
195
|
+
Several packs exist because of risks named in OWASP's two lists for AI systems: the
|
|
196
|
+
[Top 10 for Agentic Applications](https://genai.owasp.org/resource/owasp-top-10-for-agentic-applications-for-2026/)
|
|
197
|
+
(ASI01 to ASI10, 2026 edition) and the
|
|
198
|
+
[Top 10 for LLM Applications](https://genai.owasp.org/resource/owasp-genai-llm-top-10-2026/)
|
|
199
|
+
(LLM01 to LLM10, 2026 edition). A guardrail cannot see a prompt or a model's reasoning, only the
|
|
200
|
+
action that comes out of it, so these guardrails address a risk at the point where it turns into a
|
|
201
|
+
command or a file change.
|
|
202
|
+
|
|
203
|
+
| Pack | Agentic risks | LLM risks | Default action |
|
|
204
|
+
|---|---|---|---|
|
|
205
|
+
| `working-tree`, `destructive-data`, `prod-infra`, `file-scope` | ASI02 Tool Misuse and Exploitation | LLM03 Excessive Agency, LLM10 Improper Output Handling (`block-destructive-sql`) | `block` or `require_approval` |
|
|
206
|
+
| `rce-supply-chain` | ASI04 Agentic Supply Chain, ASI05 Unexpected Code Execution | LLM04 Supply Chain, LLM07 Misinformation (`rce.unverified-package`) | `block` or `require_approval` |
|
|
207
|
+
| `privilege-supply-chain` | ASI03 Identity and Privilege Abuse, ASI04 Agentic Supply Chain | LLM03 Excessive Agency (`ps.iam-grant`), LLM04 Supply Chain, LLM07 Misinformation (`flag-dependency-install`) | `require_approval`, except `flag-dependency-install`, which is `warn` |
|
|
208
|
+
| `secret-exposure`, `exfiltration` | ASI02 Tool Misuse and Exploitation, ASI03 Identity and Privilege Abuse | LLM02 Sensitive Information Disclosure | `secret-exposure` is mostly `warn`; `exfiltration` is `block` or `require_approval` |
|
|
209
|
+
| `safety-bypass` | ASI09 Human-Agent Trust Exploitation | LLM10 Improper Output Handling (`gb.ansi-terminal-forgery`) | `require_approval`, except `gb.ansi-terminal-forgery`, which is `warn` |
|
|
210
|
+
| `agent-context` | ASI03 Identity and Privilege Abuse (`ac.agent-autonomy-flag`), ASI04 Agentic Supply Chain (`ac.mcp-server-add`), ASI06 Memory and Context Poisoning (the write, not its contents), ASI10 Rogue Agents | LLM03 Excessive Agency | `require_approval` |
|
|
211
|
+
| `test-integrity` | ASI10 Rogue Agents, ASI09 Human-Agent Trust Exploitation | | Mostly `warn`; deleting a test file or editing a test runner's configuration is `require_approval` |
|
|
212
|
+
|
|
213
|
+
Each guardrail lists the ids it addresses in its own `owasp` field, so
|
|
214
|
+
`RULES.filter((rule) => rule.owasp?.includes("ASI06"))` gives the guardrails behind one risk. Each row
|
|
215
|
+
above is the union of its packs' ids, with the guardrail named where only one guardrail in the pack carries
|
|
216
|
+
an id. A test keeps the table and the guardrails' ids equal.
|
|
217
|
+
|
|
218
|
+
Read the default action before relying on a risk. A `warn` guardrail lets the action proceed and records
|
|
219
|
+
it; only `require_approval` and `block` stop it. Reading a secret is a normal part of a normal day,
|
|
220
|
+
so most secret-exposure guardrails warn, and a dependency install is flagged, not held. A file tool
|
|
221
|
+
carries a path rather than content, so `ac.memory-store-edit` cannot see what was written to a
|
|
222
|
+
memory file. You can change any guardrail's action in your own configuration.
|
|
223
|
+
|
|
224
|
+
What the guardrails cannot address, by design:
|
|
225
|
+
|
|
226
|
+
- **Prompt injection and goal hijack (LLM01, ASI01).** The guardrails never see the input that
|
|
227
|
+
redirected the agent. They can only stop the action it led to.
|
|
228
|
+
- **Anything that needs memory across commands (LLM06, ASI08).** A guardrail sees one command at a
|
|
229
|
+
time, so it cannot count, rate-limit, or spot a runaway loop.
|
|
230
|
+
- **Model, training and retrieval risks (LLM05, LLM08, LLM09), and traffic between agents
|
|
231
|
+
(ASI07).** These happen somewhere a command guardrail never reaches.
|
|
232
|
+
|
|
233
|
+
For the item-by-item matrix, including how the rest of AgentTrail covers each risk, see the
|
|
234
|
+
[AgentTrail OWASP coverage page](https://www.agenttrail.sh/security/owasp). AgentTrail is an
|
|
235
|
+
independent project, not affiliated with or endorsed by OWASP.
|
|
236
|
+
|
|
109
237
|
## Talking about a command is not running it
|
|
110
238
|
|
|
111
|
-
This is the single most important thing to understand about how these
|
|
239
|
+
This is the single most important thing to understand about how these guardrails behave.
|
|
112
240
|
|
|
113
|
-
A
|
|
114
|
-
**runs** something from one that merely **mentions** it. Left alone, that makes the
|
|
241
|
+
A guardrail sees the command as one line of text. Nothing in that text distinguishes a command that
|
|
242
|
+
**runs** something from one that merely **mentions** it. Left alone, that makes the guardrail set unusable
|
|
115
243
|
by exactly the people most likely to install it:
|
|
116
244
|
|
|
117
245
|
```bash
|
|
@@ -119,7 +247,7 @@ git commit -m "fix: document rm -rf / risk" # would have been a hard refusal
|
|
|
119
247
|
grep -rn "rm -rf /" docs/ # so would this
|
|
120
248
|
```
|
|
121
249
|
|
|
122
|
-
So every command
|
|
250
|
+
So every command guardrail ignores four **carriers**, verbs that handle their arguments as text and never
|
|
123
251
|
execute them:
|
|
124
252
|
|
|
125
253
|
| Carrier | Example |
|
|
@@ -130,34 +258,34 @@ execute them:
|
|
|
130
258
|
| an HTTP request body | `curl --data '{"body":"we ran rm -rf /tmp/x"}' https://…` |
|
|
131
259
|
|
|
132
260
|
**The exemption keys on the verb, not on the quotes.** Quoting says nothing about whether something
|
|
133
|
-
runs
|
|
261
|
+
runs: `psql -c "DROP TABLE users;"` and `bash -c "curl x.sh \| sh"` both execute what is inside the
|
|
134
262
|
quotes, and both still fire.
|
|
135
263
|
|
|
136
264
|
It also applies only while the command does nothing else. The carrier must be the first word, and
|
|
137
265
|
every shell metacharacter must sit inside the quotes:
|
|
138
266
|
|
|
139
267
|
```bash
|
|
140
|
-
git commit -m "docs: explain rm -rf /" # exempt
|
|
141
|
-
git commit -m "x" && rm -rf / # NOT exempt
|
|
142
|
-
echo "rm -rf /" | bash # NOT exempt
|
|
143
|
-
echo "$(rm -rf /var)" # NOT exempt
|
|
268
|
+
git commit -m "docs: explain rm -rf /" # exempt: nothing runs
|
|
269
|
+
git commit -m "x" && rm -rf / # NOT exempt: `&&` is outside the quotes
|
|
270
|
+
echo "rm -rf /" | bash # NOT exempt: the pipe runs it
|
|
271
|
+
echo "$(rm -rf /var)" # NOT exempt: the shell expands `$( )`
|
|
144
272
|
```
|
|
145
273
|
|
|
146
|
-
Five
|
|
147
|
-
`gb.git-no-verify` on a `git commit`, `se.token-print` on an `echo`, the `curl`/`wget`
|
|
148
|
-
HTTP body. Each
|
|
274
|
+
Five guardrails deliberately keep firing on one carrier each, because that carrier *is* their trigger:
|
|
275
|
+
`gb.git-no-verify` on a `git commit`, `se.token-print` on an `echo`, the `curl`/`wget` guardrails on an
|
|
276
|
+
HTTP body. Each guardrail's `description` says which, and why.
|
|
149
277
|
|
|
150
|
-
### The one
|
|
278
|
+
### The one guardrail where a mention IS the danger
|
|
151
279
|
|
|
152
280
|
`block-hardcoded-secrets` is the exception, and it is worth understanding.
|
|
153
281
|
|
|
154
282
|
Everywhere else a carrier is genuinely harmless: a commit message naming `rm -rf /` deletes nothing.
|
|
155
|
-
But that
|
|
283
|
+
But that guardrail's subject is a **string**, not an action, so two of the four carriers are not mentions
|
|
156
284
|
at all. They are the exposure itself:
|
|
157
285
|
|
|
158
286
|
| Carrier | What happens to the key | Exempt? |
|
|
159
287
|
|---|---|---|
|
|
160
|
-
| `grep -rn AKIA .` | searched for, goes nowhere
|
|
288
|
+
| `grep -rn AKIA .` | searched for, goes nowhere, and this is how you find a key to rotate | **yes** |
|
|
161
289
|
| `echo "AKIA…"` | transient terminal output | **yes** |
|
|
162
290
|
| `git commit -m "…AKIA…"` | written into history, then pushed | **no** |
|
|
163
291
|
| `curl --data "…AKIA…"` | sent to a remote host | **no** |
|
|
@@ -165,61 +293,43 @@ at all. They are the exposure itself:
|
|
|
165
293
|
The cost, stated in the other direction: documenting a real-looking key in a commit message is still
|
|
166
294
|
blocked. Redact the body of the key, or use a placeholder short enough to fail the length check.
|
|
167
295
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
At most four quoted arguments are recognised. A carrier that can be made to execute through a flag —
|
|
173
|
-
`ack --pager='…'`, `rg --pre <cmd>` — is still treated as a mention; closing that needs information
|
|
174
|
-
the checker does not have. And an MCP tool whose input carries the same text is not exempt either,
|
|
175
|
-
because exempting a JSON blob would exempt a shell-running MCP server along with it.
|
|
176
|
-
|
|
177
|
-
**MCP coverage.** A command rule fires on `Bash`, `PowerShell` *and* any `mcp__*` tool: the guard hands
|
|
178
|
-
the checker an MCP call's serialized `tool_input` as the same command text every command rule reads, so
|
|
179
|
-
a command shape run through an MCP server — `{"command":"rm -rf /"}` — is caught, not ignored. Two
|
|
180
|
-
honest limits follow from that. First, a rule whose pattern is anchored to the start of the command
|
|
181
|
-
(`^…` or a command-position class) may not fire inside the JSON, where the shape sits after a `"`
|
|
182
|
-
rather than at a command boundary; the `\b`-anchored rules — most of the corpus — do fire. Second, the
|
|
183
|
-
quoted-mention exemptions are shell-only, so an MCP payload that merely *names* a command in a text
|
|
184
|
-
field (`{"title":"fix the rm -rf / bug"}`) is matched the same as one that runs it — a JSON blob cannot
|
|
185
|
-
be told apart from a shell-running MCP server. File rules match by path on whichever file tool a client
|
|
186
|
-
uses. No rule is shell-only by design; a rule that does not reach the MCP channel does so because its
|
|
187
|
-
pattern, not its label, does not match the serialized shape.
|
|
188
|
-
|
|
189
|
-
## What these rules deliberately do not catch
|
|
296
|
+
The finer limits of the carrier logic, and how command guardrails apply to MCP tools, are in the
|
|
297
|
+
[Reference](#reference).
|
|
298
|
+
|
|
299
|
+
## What these guardrails deliberately do not catch
|
|
190
300
|
|
|
191
301
|
Stated here rather than discovered later. Every one is a real limit of the format, not something
|
|
192
302
|
somebody forgot.
|
|
193
303
|
|
|
194
|
-
- **Nothing about the web.** Pages an agent fetches are not checked, and there are no URL
|
|
304
|
+
- **Nothing about the web.** Pages an agent fetches are not checked, and there are no URL guardrails.
|
|
195
305
|
- **Nothing inside a file.** The checker sees a file's *path*, never its contents. A secret typed into
|
|
196
|
-
a source file, SQL built by string concatenation, a missing auth check
|
|
197
|
-
|
|
306
|
+
a source file, SQL built by string concatenation, a missing auth check: none of it is visible.
|
|
307
|
+
Guardrails that would need it are absent rather than approximated.
|
|
198
308
|
- **Nothing about where you are.** No working directory, no project root, no git branch, no cloud
|
|
199
|
-
profile reaches the checker
|
|
200
|
-
project" **cannot be written as a
|
|
201
|
-
for good. For the same reason, a
|
|
202
|
-
- **Nothing hidden inside a quoted payload.** Where the danger is inside a quoted argument
|
|
203
|
-
`psql -c "<sql>"`, `python -c "<code>"`
|
|
204
|
-
very little about what the script actually does.
|
|
309
|
+
profile reaches the checker; it gets one command and nothing else. So "the agent wrote outside the
|
|
310
|
+
project" **cannot be written as a guardrail**, and `file-scope` is limited to well-known absolute paths
|
|
311
|
+
for good. For the same reason, a guardrail cannot tell a scratch database from a production one.
|
|
312
|
+
- **Nothing hidden inside a quoted payload.** Where the danger is inside a quoted argument
|
|
313
|
+
(`psql -c "<sql>"`, `python -c "<code>"`), a text guardrail can only guess. In a long script, a match
|
|
314
|
+
says very little about what the script actually does.
|
|
205
315
|
- **Nothing a wrapper hides.** `./deploy.sh` that runs `terraform apply -auto-approve` inside it is
|
|
206
316
|
just a shell script from the outside.
|
|
207
317
|
- **Nothing recurring.** There is no counting. "The same mistake three times this week" needs memory
|
|
208
318
|
across commands, and a single command has none.
|
|
209
319
|
|
|
210
|
-
Each
|
|
211
|
-
case
|
|
320
|
+
Each guardrail's own `description` names its specific misses. Read those before trusting a guardrail to cover a
|
|
321
|
+
case; they are written to be believed, not to sell.
|
|
212
322
|
|
|
213
|
-
## Every
|
|
323
|
+
## Every guardrail proves both directions
|
|
214
324
|
|
|
215
|
-
Every
|
|
325
|
+
Every guardrail ships at least one **`block`** example and at least one **`allow`** example. A guardrail missing
|
|
216
326
|
either does not build.
|
|
217
327
|
|
|
218
|
-
- **`block` means "this
|
|
328
|
+
- **`block` means "this guardrail must match."** It does *not* mean the agent is refused; most packs
|
|
219
329
|
default to asking or warning.
|
|
220
|
-
- **`allow` means "this
|
|
221
|
-
|
|
222
|
-
day, and a
|
|
330
|
+
- **`allow` means "this guardrail must NOT match."** *This is the half that matters.* Anyone can write a
|
|
331
|
+
guardrail that catches `rm -rf /`. The hard part is not firing on `rm -rf ./node_modules` forty times a
|
|
332
|
+
day, and a guardrail with no negative example has not shown it can tell them apart.
|
|
223
333
|
|
|
224
334
|
Examples come in two kinds, and each must use the right one:
|
|
225
335
|
|
|
@@ -228,13 +338,13 @@ Examples come in two kinds, and each must use the right one:
|
|
|
228
338
|
| a command | `Bash`, `PowerShell`, a search query, an MCP tool's input |
|
|
229
339
|
| a file path | `Edit`, `Write`, `Read`, `MultiEdit`, `NotebookEdit` |
|
|
230
340
|
|
|
231
|
-
Giving a path-matching
|
|
232
|
-
nothing, which reads as proof of quietness and proves only that the path never reached the
|
|
341
|
+
Giving a path-matching guardrail a command example makes it pass **without testing anything**: it matches
|
|
342
|
+
nothing, which reads as proof of quietness and proves only that the path never reached the guardrail. CI
|
|
233
343
|
rejects that.
|
|
234
344
|
|
|
235
|
-
## Contributing a
|
|
345
|
+
## Contributing a guardrail
|
|
236
346
|
|
|
237
|
-
|
|
347
|
+
Guardrails are meant to be contributed. The bar is not "clever regex"; it is **does it fire on the real
|
|
238
348
|
thing, and stay quiet on the near-miss**.
|
|
239
349
|
|
|
240
350
|
**1. Write it**, following the shape above. Give it a `description` that says what it misses.
|
|
@@ -251,65 +361,158 @@ if (!result.success) console.error(result.error.issues);
|
|
|
251
361
|
Or, if you have the guard installed:
|
|
252
362
|
|
|
253
363
|
```bash
|
|
254
|
-
agenttrail-guard guardrails validate ./my-
|
|
364
|
+
agenttrail-guard guardrails validate ./my-guardrail.json
|
|
255
365
|
```
|
|
256
366
|
|
|
257
|
-
**3. Understand what that does and does not tell you.** It answers *"is this a well-formed
|
|
367
|
+
**3. Understand what that does and does not tell you.** It answers *"is this a well-formed guardrail?"* It
|
|
258
368
|
does **not** answer *"does it actually fire on the command I think it does?"* That needs the real
|
|
259
369
|
checker, which is not part of this package.
|
|
260
370
|
|
|
261
|
-
**So the real test runs in CI, on your pull request
|
|
371
|
+
**So the real test runs in CI, on your pull request**: the same check, on the same machine, for
|
|
262
372
|
everyone. You get the shape check instantly here and the real answer there, which is where it has to
|
|
263
373
|
run to be trusted anyway.
|
|
264
374
|
|
|
265
|
-
Your
|
|
266
|
-
match at all. Your own negative example only proves your
|
|
267
|
-
of. The quiet corpus is what catches a Terraform
|
|
375
|
+
Your guardrail is also run against a **quiet corpus**: hundreds of everyday commands and paths that no
|
|
376
|
+
guardrail may match at all. Your own negative example only proves your guardrail is quiet on the near-miss
|
|
377
|
+
*you* thought of. The quiet corpus is what catches a Terraform guardrail firing on `pnpm test`.
|
|
268
378
|
|
|
269
379
|
### Shapes that will not validate
|
|
270
380
|
|
|
271
|
-
Three are rejected outright, each because it produces a
|
|
381
|
+
Three are rejected outright, each because it produces a guardrail that *looks* enforced and is not: the
|
|
272
382
|
worst failure a security tool can have.
|
|
273
383
|
|
|
274
384
|
| Rejected | Why |
|
|
275
385
|
|---|---|
|
|
276
|
-
| `scope` | It compares against ids that are always UUIDs, never a vendor name
|
|
386
|
+
| `scope` | It compares against ids that are always UUIDs, never a vendor name, so a scoped guardrail matches nothing, forever, silently. |
|
|
277
387
|
| Numeric conditions | Token counts and durations are all zero *before* a command runs. "Greater than" can never fire; "less than" fires on everything. |
|
|
278
388
|
| A command matcher and a file matcher in one condition | No real command carries both, so the condition can never be true. Split it into two under `any_of`. |
|
|
279
389
|
|
|
280
390
|
A regular expression is also rejected if it nests unbounded repetition (`(a+)+`). The guard fails
|
|
281
|
-
**open** under a time limit, so a pattern that backtracks does not merely run slowly
|
|
391
|
+
**open** under a time limit, so a pattern that backtracks does not merely run slowly; it lets the
|
|
282
392
|
command through.
|
|
283
393
|
|
|
284
394
|
One more is caught in CI rather than by the shape check, because it cannot be caught earlier: **a
|
|
285
|
-
single-item brace list in a tool name.** `"{Bash}"` is a glob pattern, and it does not match `Bash
|
|
286
|
-
so the
|
|
395
|
+
single-item brace list in a tool name.** `"{Bash}"` is a glob pattern, and it does not match `Bash`,
|
|
396
|
+
so the guardrail matches nothing, forever, with no error anywhere. Write a single tool plainly as
|
|
287
397
|
`"Bash"`; braces are for real alternatives, `"{Bash,PowerShell}"`.
|
|
288
398
|
|
|
289
|
-
|
|
399
|
+
### Ready to open a pull request?
|
|
400
|
+
|
|
401
|
+
[CONTRIBUTING.md](CONTRIBUTING.md) has a pre-submission checklist, local build and test
|
|
402
|
+
commands, and where to report what. Please follow the [Code of Conduct](CODE_OF_CONDUCT.md),
|
|
403
|
+
and report vulnerabilities privately as described in [SECURITY.md](SECURITY.md).
|
|
404
|
+
|
|
405
|
+
## Stability and versioning
|
|
406
|
+
|
|
407
|
+
Guardrail ids and pack names end up in people's config files, so they are treated as public API:
|
|
408
|
+
|
|
409
|
+
- **A guardrail id never changes.** Renaming or removing a guardrail would silently undo an override
|
|
410
|
+
someone set on it, so either one is a breaking change.
|
|
411
|
+
- **A pack name never changes** either, for the same reason: a renamed pack silently
|
|
412
|
+
re-enables a pack someone had turned off.
|
|
413
|
+
- **A new guardrail or pack is a feature.** Guard turns every pack on unless you turned it off, so a
|
|
414
|
+
guardrail added in a release you install starts enforcing with no step of your own.
|
|
415
|
+
|
|
416
|
+
Releases are cut automatically from [Conventional Commits](https://www.conventionalcommits.org/).
|
|
417
|
+
Before 1.0, a breaking change ships as a minor release and a new guardrail as a patch release, so
|
|
418
|
+
skim the [changelog](CHANGELOG.md) before you upgrade.
|
|
419
|
+
|
|
420
|
+
## FAQ
|
|
421
|
+
|
|
422
|
+
**Can I use these guardrails without Guard?** Yes. They are plain data under Apache-2.0. You need your
|
|
423
|
+
own matcher to apply them, because this package does not contain one.
|
|
424
|
+
|
|
425
|
+
**A guardrail fires on something legitimate. What do I do?** In Guard, `agenttrail-guard guardrails
|
|
426
|
+
allow <id> <pattern>` silences that one guardrail for that one command shape, without turning the guardrail
|
|
427
|
+
off. Then please [open an issue](https://github.com/agenttrailhq/guardrails/issues) with a
|
|
428
|
+
sanitized example, so the guardrail can learn the near-miss.
|
|
429
|
+
|
|
430
|
+
**Why didn't a guardrail fire on my command?** Start with the guardrail's `description`: it lists what the
|
|
431
|
+
guardrail misses. Then check [what these guardrails deliberately do not catch](#what-these-guardrails-deliberately-do-not-catch).
|
|
432
|
+
To test a command against every guardrail, paste it into the [online checker](https://www.agenttrail.sh/guardrails#check).
|
|
433
|
+
|
|
434
|
+
**Why is `secret-exposure` mostly `warn`?** Reading a secret is a normal part of a normal day.
|
|
435
|
+
Refusing every `.env` read would be turned off within a week. Moving a secret off the machine is
|
|
436
|
+
held for approval instead.
|
|
437
|
+
|
|
438
|
+
**How do I turn off a whole pack?** `agenttrail-guard guardrails disable <pack>`. See
|
|
439
|
+
[Tune a guardrail](https://github.com/agenttrailhq/guard#tune-a-guardrail) in the Guard README.
|
|
440
|
+
|
|
441
|
+
## Reference
|
|
442
|
+
|
|
443
|
+
### Two ways to import it
|
|
290
444
|
|
|
291
445
|
```ts
|
|
292
|
-
import { RULES, getRule } from "@agenttrail/guardrails/guardrails"; // just the
|
|
293
|
-
import { parseRule } from "@agenttrail/guardrails"; //
|
|
446
|
+
import { RULES, getRule } from "@agenttrail/guardrails/guardrails"; // just the guardrails
|
|
447
|
+
import { parseRule } from "@agenttrail/guardrails"; // guardrails + the shape checker
|
|
294
448
|
```
|
|
295
449
|
|
|
296
|
-
|
|
450
|
+
The exports keep their original names (`RULES`, `getRule`, `parseRule`, `defineRule`, `Rule`); each
|
|
451
|
+
entry is a guardrail.
|
|
452
|
+
|
|
453
|
+
Use the first when you want to *apply* guardrails, and the second when you want to *validate* one you are
|
|
297
454
|
writing.
|
|
298
455
|
|
|
299
456
|
They are separate because the guard starts a fresh process on **every single command** an agent runs,
|
|
300
457
|
under a ten-second ceiling. It cannot afford to load a validator it never calls, or to re-check 74
|
|
301
|
-
|
|
458
|
+
guardrails that were already checked before release.
|
|
459
|
+
|
|
460
|
+
### Known limits of the carrier logic
|
|
461
|
+
|
|
462
|
+
In the same spirit as the rest of this file:
|
|
463
|
+
|
|
464
|
+
- **The carrier must be the first word.** A single leading `sudo` is tolerated, because it changes
|
|
465
|
+
privilege rather than meaning, but a runner prefix is not: `pnpm exec rg …`, `npx …` and
|
|
466
|
+
`xargs -0 grep …` still fire, since "some program eventually runs a search" is a much weaker claim
|
|
467
|
+
than "this command is a search".
|
|
468
|
+
- **At most four quoted arguments are recognised.**
|
|
469
|
+
- **A carrier that can be made to execute through a flag** (`ack --pager='…'`, `rg --pre <cmd>`) is
|
|
470
|
+
still treated as a mention; closing that needs information the checker does not have.
|
|
471
|
+
- **An MCP tool whose input carries the same text is not exempt either,** because exempting a JSON
|
|
472
|
+
blob would exempt a shell-running MCP server along with it.
|
|
473
|
+
|
|
474
|
+
### MCP coverage
|
|
475
|
+
|
|
476
|
+
A command guardrail fires on `Bash`, `PowerShell` *and* any `mcp__*` tool: the guard hands the checker an
|
|
477
|
+
MCP call's serialized `tool_input` as the same command text every command guardrail reads, so a command
|
|
478
|
+
shape run through an MCP server (`{"command":"rm -rf /"}`) is caught, not ignored. Two honest limits
|
|
479
|
+
follow from that:
|
|
302
480
|
|
|
303
|
-
|
|
481
|
+
1. **Start-anchored patterns may miss.** A guardrail whose pattern is anchored to the start of the command
|
|
482
|
+
(`^…` or a command-position class) may not fire inside the JSON, where the shape sits after a `"`
|
|
483
|
+
rather than at a command boundary; the `\b`-anchored guardrails (most of the corpus) do fire.
|
|
484
|
+
2. **Mentions are not exempt.** The quoted-mention exemptions are shell-only, so an MCP payload that
|
|
485
|
+
merely *names* a command in a text field (`{"title":"fix the rm -rf / bug"}`) is matched the same
|
|
486
|
+
as one that runs it; a JSON blob cannot be told apart from a shell-running MCP server.
|
|
487
|
+
|
|
488
|
+
File guardrails match by path on whichever file tool a client uses. No guardrail is shell-only by design; a
|
|
489
|
+
guardrail that does not reach the MCP channel does so because its pattern, not its label, does not match
|
|
490
|
+
the serialized shape.
|
|
491
|
+
|
|
492
|
+
### Where these guardrails came from
|
|
304
493
|
|
|
305
494
|
Independently authored. No block list, pattern or wording is copied from any other project.
|
|
306
495
|
|
|
307
|
-
Where a
|
|
308
|
-
pattern
|
|
496
|
+
Where a guardrail's shape follows an obvious convention (an `rm -rf` pattern looks like an `rm -rf`
|
|
497
|
+
pattern), that is two people meeting the same shell, not one copying the other.
|
|
498
|
+
|
|
499
|
+
## Part of AgentTrail
|
|
309
500
|
|
|
310
|
-
|
|
311
|
-
|
|
501
|
+
This library is part of [AgentTrail](https://www.agenttrail.sh), which builds guardrails and
|
|
502
|
+
observability for AI coding agents:
|
|
503
|
+
|
|
504
|
+
- **[AgentTrail Guard](https://www.agenttrail.sh/agenttrail-guard)** enforces these guardrails on every
|
|
505
|
+
tool call that reaches its hooks, on your machine. It is free, open source, and needs no
|
|
506
|
+
account. Its code is in [agenttrailhq/guard](https://github.com/agenttrailhq/guard).
|
|
507
|
+
- **[AgentTrail OS](https://www.agenttrail.sh/agenttrail-os)** uses the same open guardrail library,
|
|
508
|
+
and adds searchable session history and team workflows. See
|
|
509
|
+
[AgentTrail Guard vs AgentTrail OS](https://www.agenttrail.sh/guard-vs-os).
|
|
312
510
|
|
|
313
511
|
## License
|
|
314
512
|
|
|
315
513
|
Apache-2.0. See [LICENSE](./LICENSE).
|
|
514
|
+
|
|
515
|
+
Guardrail ideas and reports go to [Guardrails issues](https://github.com/agenttrailhq/guardrails/issues).
|
|
516
|
+
If you rely on these guardrails, a star helps other developers find them.
|
|
517
|
+
|
|
518
|
+
Made with ❤️ by [agenttrail.sh](https://www.agenttrail.sh)
|