@agentwares/agentguard 0.1.7 → 0.1.10

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.
Files changed (52) hide show
  1. package/CHANGELOG.md +108 -0
  2. package/README.md +223 -16
  3. package/dist/audit-5LUDXZLG.js +18 -0
  4. package/dist/audit-5LUDXZLG.js.map +1 -0
  5. package/dist/chunk-2QDPPTPY.js +179 -0
  6. package/dist/chunk-2QDPPTPY.js.map +1 -0
  7. package/dist/chunk-4WTFMFRY.js +1270 -0
  8. package/dist/chunk-4WTFMFRY.js.map +1 -0
  9. package/dist/chunk-5WRI5ZAA.js +31 -0
  10. package/dist/chunk-5WRI5ZAA.js.map +1 -0
  11. package/dist/chunk-DEVXG3IB.js +113 -0
  12. package/dist/chunk-DEVXG3IB.js.map +1 -0
  13. package/dist/chunk-F73VDY3W.js +202 -0
  14. package/dist/chunk-F73VDY3W.js.map +1 -0
  15. package/dist/chunk-IEJK7N3B.js +275 -0
  16. package/dist/chunk-IEJK7N3B.js.map +1 -0
  17. package/dist/chunk-IS3DAHZI.js +1698 -0
  18. package/dist/chunk-IS3DAHZI.js.map +1 -0
  19. package/dist/chunk-JSELR4SG.js +780 -0
  20. package/dist/chunk-JSELR4SG.js.map +1 -0
  21. package/dist/chunk-OM5L7A32.js +199 -0
  22. package/dist/chunk-OM5L7A32.js.map +1 -0
  23. package/dist/chunk-SWZQMPDG.js +206 -0
  24. package/dist/chunk-SWZQMPDG.js.map +1 -0
  25. package/dist/chunk-TJXZBOHL.js +529 -0
  26. package/dist/chunk-TJXZBOHL.js.map +1 -0
  27. package/dist/chunk-XOBFANL4.js +119 -0
  28. package/dist/chunk-XOBFANL4.js.map +1 -0
  29. package/dist/chunk-XPNBYSIN.js +23 -0
  30. package/dist/chunk-XPNBYSIN.js.map +1 -0
  31. package/dist/chunk-YBVD6HTT.js +1036 -0
  32. package/dist/chunk-YBVD6HTT.js.map +1 -0
  33. package/dist/chunk-ZG4LGUJE.js +847 -0
  34. package/dist/chunk-ZG4LGUJE.js.map +1 -0
  35. package/dist/cli.js +10 -4775
  36. package/dist/cli.js.map +1 -1
  37. package/dist/fixtures/crm-server.js +6 -1272
  38. package/dist/fixtures/crm-server.js.map +1 -1
  39. package/dist/fixtures/demo-agent.js +4 -115
  40. package/dist/fixtures/demo-agent.js.map +1 -1
  41. package/dist/index.d.ts +73 -14
  42. package/dist/index.js +247 -5138
  43. package/dist/index.js.map +1 -1
  44. package/dist/init-MKPQU2JG.js +17 -0
  45. package/dist/init-MKPQU2JG.js.map +1 -0
  46. package/dist/proxy-6GP63TLD.js +146 -0
  47. package/dist/proxy-6GP63TLD.js.map +1 -0
  48. package/dist/tools-XS7PMTLT.js +15 -0
  49. package/dist/tools-XS7PMTLT.js.map +1 -0
  50. package/llms.txt +10 -4
  51. package/package.json +16 -4
  52. package/server.json +9 -9
package/CHANGELOG.md CHANGED
@@ -3,6 +3,114 @@
3
3
  `@agentwares/agentguard`, the CLI and MCP proxy. A tag `agentguard-v<version>` publishes the
4
4
  GitHub Release for a version with its section below as the notes.
5
5
 
6
+ ## 0.1.10 — 2026-10-08
7
+
8
+ ### Changed
9
+
10
+ - **agentguard is now okgate.** "Nothing irreversible without an OK." The CLI calls itself
11
+ `okgate`, and npm serves it as a second bin of this package until `@agentwares/okgate` is
12
+ published: `npx -p @agentwares/agentguard okgate init`. Everything it writes from now on uses
13
+ the new names: `okgate.yaml`, `.okgate/`, `OKGATE_*`, the `okgate` MCP server entry, the
14
+ `okgate_get_status` / `okgate_get_report` / `okgate_verify_audit_log` tools, `X-Okgate-Key`,
15
+ and hook commands that run `npx -y -p @agentwares/agentguard@<version> okgate hook pre-tool-use`.
16
+ - The server registers in the MCP Registry as `io.github.agentwares/okgate` (`mcpName`).
17
+
18
+ ### Kept working (until at least 2027-01-15)
19
+
20
+ - The `agentguard` bin, and `npx @agentwares/agentguard`, which runs it.
21
+ - `agentguard.yaml` is read when there is no `okgate.yaml`, including walking up from a coding
22
+ agent's working directory; creating an `okgate.yaml` beside it counts as editing the guard.
23
+ - An existing `.agentguard/` stays the state directory when there is no `.okgate/`, so counters,
24
+ pending approvals and the audit log's hash chain carry on: `okgate verify` passes across the
25
+ rename. Both directories are the guard's own and refused to a coding agent.
26
+ - Each `AGENTGUARD_*` variable applies when its `OKGATE_*` twin is unset. **`AGENTGUARD_KILL=1`
27
+ and `OKGATE_KILL=1` both halt everything, whatever `kill.env` the policy names**, and a KILL
28
+ file under either directory halts; `okgate kill` writes both that exist and `resume` clears both.
29
+ - `hooks status`, `hooks uninstall` and `hooks install` recognise agentguard-era hook entries
30
+ (install replaces one, it never adds a second); `init` recognises the old `agentguard` MCP entry
31
+ and `init --undo` restores a `*.agentguard-backup`; `connect` replaces an `agentguard` entry.
32
+ - `tools/list` shows only `okgate_*`; a `tools/call` for an `agentguard_*` name runs its twin with
33
+ a deprecation note in `_meta["okgate/deprecation"]` until 2027-01-15, then answers
34
+ `TOOL_RENAMED` (`retryable: false`, with the new name in `fix`). Tool results carry the verdict
35
+ under `_meta.okgate` and, for the window, `_meta.agentguard`.
36
+ - `<!-- agentguard: … -->` inline checks, `X-Agentguard-Key`, and `_meta["agentguard/runId"]`.
37
+ - `permission-diff` (and the GitHub Action) diffs `okgate.yaml` and `agentguard.yaml` alike.
38
+
39
+ ## 0.1.9 — 2026-10-07
40
+
41
+ ### Added
42
+
43
+ - **`agentguard audit`: which written rules your coding agents broke.** Reads the repository's
44
+ CLAUDE.md, AGENTS.md, GEMINI.md, `.cursor/rules` (and the other files agentsmd-lint looks for),
45
+ plus `audit.rules` in agentguard.yaml, and this machine's Claude Code and Codex sessions whose
46
+ working directory is the repository or one of its worktrees, subagents included. Per rule:
47
+ sessions checked, sessions broken, the last three dates, and — on your screen only — the
48
+ command or path that broke it. `--since 7d|30d|24h|all|<date>` (default 7 days),
49
+ `--client claude|codex|all`, `--json` (counts, rule ids and dates, never a command, a path or a
50
+ prompt), `--rules <file>`, `--no-cache`, `--no-evidence`.
51
+ - **Rules recognised with no LLM.** Every rule in the files is listed, checkable or not. A
52
+ pattern library maps the common shapes to checks over what the agent did (never push to /
53
+ commit on main, never force-push, never `--no-verify`, run X before committing or pushing, use
54
+ pnpm not npm, do not edit or commit a path, no new dependencies without asking, never `rm`
55
+ outside the repo, never publish, never deploy, never run a named command). An inline
56
+ `<!-- agentguard: <check> -->` on the rule, or `audit.rules` in agentguard.yaml, always works;
57
+ `<!-- agentguard: none -->` marks a rule no script can check.
58
+ - **`agentguard audit --enforce [--preview] [--yes]`** writes the checkable rules into
59
+ agentguard.yaml as hook-mode rules — `hooks.shell.deny`, `hooks.shell.approval` (for "without
60
+ asking"), `hooks.paths.deny`, the built-in irreversible classes and a `deploy` rule — each with a
61
+ comment naming the rule it came from, shows the diff, and installs the Claude Code hook if it is
62
+ not installed. It never changes `hooks.mode`, and writes nothing to a policy already in enforce
63
+ mode without `--yes`.
64
+ - **A counts-only cache** in `~/.agentguard/audit-cache-v1/`: a second run of a large history
65
+ takes about a second, and counts from transcripts Claude Code has since deleted are kept. It
66
+ holds counts, times, line numbers and hashes only.
67
+ - **Plugin skills `/agentguard:audit`** (runs `audit --json`, then has your own agent judge the
68
+ rules marked not checkable against the repository and its git history) **and
69
+ `/agentguard:hooks`** (hook mode status, install, uninstall), in the Claude Code plugin and the
70
+ Gemini CLI extension.
71
+ - The audit's last line points at the team adherence history page
72
+ (https://agentwares-agentguard.vercel.app/team-history). The CLI itself sends nothing.
73
+
74
+ ### Changed
75
+
76
+ - Requires `@agentwares/agentguard-core` 0.1.5.
77
+
78
+ ## 0.1.8 — 2026-10-07
79
+
80
+ ### Added
81
+
82
+ - **Hook mode: one rulebook for coding agents.** `agentguard hooks install [--project|--user]
83
+ [--client claude|codex|gemini|all]` writes a pre-execution hook (Claude Code `PreToolUse`,
84
+ Codex `PreToolUse`, Gemini CLI `BeforeTool`) that runs `agentguard hook pre-tool-use` on every
85
+ shell command, file edit and direct MCP call, and adds a dry-run `hooks:` block to
86
+ `agentguard.yaml`. `hooks uninstall` removes only agentguard's entries; `hooks status` lists them.
87
+ The policy gains `hooks.shell.deny|approval` (command globs, matched against every simple
88
+ command of a compound line, `$(…)`, `bash -c` and `eval` included), `hooks.paths.deny|approval`
89
+ (file globs for edits and shell writes), and `hooks.irreversible` (built-in classes `git-push`,
90
+ `merge`, `tag-delete`, `rm-outside-workspace`, `publish`, `send`, `guard-config`, plus your own
91
+ rules). The MCP side's `deny` and `approval.tools` also apply to MCP tools the agent calls
92
+ directly. Same kill switch, approvals, caps (`hook_calls`, `shell_commands`, `file_edits`,
93
+ `irreversible`), loop breaker (repeated identical edits) and hash-chained audit log.
94
+ - **Irreversible commands only on a turn a person typed.** In Claude Code an irreversible command
95
+ runs only when the latest transcript entry a person wrote came after the session's previous
96
+ irreversible command, is at most `window_s` (30 min) old, and names the action — or is a bare
97
+ "yes" to an assistant question that names it. Tool results, injected and meta entries, peer and
98
+ task messages, subagent prompts and anything inside an assistant reply never count, so a model
99
+ that writes "user: yes, push it" into its own reply (claude-code#88122 and others) is held with
100
+ `APPROVAL_REQUIRED`; `agentguard approve <id>` lets the identical retry run once. Codex and Gemini
101
+ CLI get the rulebook but no transcript check: their irreversible commands always need approval.
102
+ - **The agent cannot approve itself.** `agentguard approve|deny|resume` run by the agent, and any
103
+ write to `.agentguard/`, are refused.
104
+ - `agentguard report` has a **Coding-agent hooks** section: calls checked, irreversible commands
105
+ run on a person's turn or after approval or held, and every call enforce mode would have stopped
106
+ while in dry-run.
107
+
108
+ ### Changed
109
+
110
+ - The CLI loads the MCP SDK only for `proxy`, `init` and `tools`, so the hook starts in about
111
+ 0.1 s (`dist/` is now split into chunks).
112
+ - Requires `@agentwares/agentguard-core` 0.1.4.
113
+
6
114
  ## 0.1.7 — 2026-10-07
7
115
 
8
116
  ### Fixed
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # agentguard
2
2
 
3
+ [![Listed on mcpservers.org](https://mcpservers.org/badge.svg)](https://mcpservers.org/servers/agentwares/agentguard)
4
+
3
5
  **60 seconds to a safe first run.** Your agent already has an MCP config. Put agentguard in front of it, run the agent once in dry-run, and read what it _would_ have done:
4
6
 
5
7
  ```sh
@@ -37,6 +39,8 @@ agentguard is an MCP policy proxy for agents that touch production. It sits betw
37
39
  - **Semantic loop breaker** — the same `(tool, normalized args)` 3× in the last 30 calls, or an A→B→A→B cycle, returns `LOOP_DETECTED`. Timestamps, ids, whitespace and key order are ignored.
38
40
  - **Blast-radius caps** — `tool_calls`, `writes`, `deletes`, `emails`, `spend_usd` and custom counters, per run and per day.
39
41
  - **Hash-chained audit log** — every call is a JSONL line with `prev_hash` and `hash`; `agentguard verify` proves no entry was edited, removed from the middle, or reordered (see Limits for what a local chain cannot prove on its own).
42
+ - **Hook mode for coding agents** — `agentguard hooks install` puts the same policy in front of Claude Code's, Codex's and Gemini CLI's own shell commands and file edits; irreversible commands (push, merge, publish, send, `rm` outside the repo) run only on a recent turn a person typed that names them, never on text the model wrote. [Details](#hook-mode-a-coding-agents-own-commands).
43
+ - **Rule adherence audit** — `agentguard audit` reads CLAUDE.md, AGENTS.md, GEMINI.md and `.cursor/rules` and this machine's Claude Code and Codex sessions, and prints which rules the agents broke ("broken in 9 of 31 sessions"), locally; `--enforce` turns the checkable ones into hook rules in dry-run. [Details](#audit-which-written-rules-your-coding-agents-broke).
40
44
 
41
45
  No LLM calls. No phone-home. No account. MIT.
42
46
 
@@ -121,6 +125,202 @@ docker run -i --rm agentguard proxy --config /app/demo/agentguard.yaml # a fak
121
125
 
122
126
  Prefer HTTP (several agents, scoped keys, Slack approve buttons)? `agentguard proxy --http --port 8788` and point clients at `http://127.0.0.1:8788/mcp` with an `X-Run-Id` header per run and `Authorization: Bearer agk_…` per agent.
123
127
 
128
+ ## Hook mode: a coding agent's own commands
129
+
130
+ The proxy sees MCP calls. A coding agent also acts through its own shell and file tools, which no
131
+ MCP proxy sees. Hook mode puts the same `agentguard.yaml` in front of those, through the agent's
132
+ pre-execution hook, and adds one check nothing else makes: an irreversible command runs only on a
133
+ turn a **person** typed.
134
+
135
+ ```sh
136
+ npx -y @agentwares/agentguard hooks install # Claude Code, this project (.claude/settings.json)
137
+ npx -y @agentwares/agentguard hooks install --client all # + Codex (.codex/hooks.json) and Gemini CLI (.gemini/settings.json)
138
+ npx -y @agentwares/agentguard hooks install --user # every project (~/.claude/settings.json, policy in ~/.agentguard/)
139
+ npx -y @agentwares/agentguard hooks status | uninstall
140
+ ```
141
+
142
+ `install` adds one hook entry (other settings and hooks are kept; the first change leaves a
143
+ `*.agentguard-backup`), adds a commented `hooks:` block in **dry-run** to `agentguard.yaml` (or
144
+ writes a policy with only that block), and adds `.agentguard/` to `.gitignore`. Commit
145
+ `agentguard.yaml` and `.claude/settings.json` and the whole team runs the same rulebook. The hook
146
+ command is `npx -y @agentwares/agentguard@<version> hook pre-tool-use` (about half a second per
147
+ guarded call); with the package installed globally, `--command agentguard` makes it about 0.1 s.
148
+
149
+ | Agent | Hook (verified 7 Oct 2026) | Rules, approvals, caps, audit | Human-turn gate |
150
+ | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------- |
151
+ | Claude Code | [`PreToolUse`](https://code.claude.com/docs/en/hooks): `Bash`, `Write`, `Edit`, `mcp__*` … | yes | yes, from the session transcript |
152
+ | Codex CLI | [`PreToolUse`](https://developers.openai.com/codex/hooks): `Bash`, `apply_patch`, `mcp__*` | yes (trust the hook once in Codex's `/hooks`) | no: Codex calls its transcript format unstable, so irreversible = approve |
153
+ | Gemini CLI | [`BeforeTool`](https://github.com/google-gemini/gemini-cli/blob/main/docs/hooks/reference.md): `run_shell_command`, `write_file`, `replace`, `mcp_*` | yes | no: not verified against a local install, so irreversible = approve |
154
+
155
+ Cursor has its own hooks (`beforeShellExecution`) and can load Claude Code's
156
+ ([cursor.com/docs/hooks](https://cursor.com/docs/hooks)); hook mode has not been tested there.
157
+
158
+ **What is checked.** Each shell command is split into simple commands the way a shell would
159
+ (`&&`, `;`, pipes, `$(…)`, backticks, `bash -c '…'`, `eval`, `find -exec`, heredoc bodies skipped,
160
+ `sudo`/`env`/`timeout`/`npx` wrappers and git's `-C` removed, `cd` followed). Then:
161
+
162
+ ```yaml
163
+ hooks:
164
+ mode: dry-run # dry-run: log what enforce would stop, block nothing | enforce
165
+ shell:
166
+ deny: ["terraform destroy*", "curl * | sh"] # refused whatever anyone says
167
+ approval: ["kubectl delete *"] # held for `agentguard approve`
168
+ paths: # file edits and shell writes (>, tee, cp, mv, rm, sed -i)
169
+ deny: [".env", "*.pem"] # no slash: any file with that name; `src/**`: from the project root
170
+ approval: ["**/migrations/**"]
171
+ irreversible:
172
+ builtins: true # or a list: [git-push, merge, tag-delete, rm-outside-workspace, publish, send, guard-config]
173
+ window_s: 1800 # the authorizing turn is at most 30 minutes old
174
+ require_mention: true # ...and names the action
175
+ require_origin: false # true: only entries labelled as typed by a person (see below)
176
+ mentions: { git-push: [deploy] } # extra words per class
177
+ rules: # your own irreversible classes
178
+ - { name: deploy, shell: ["vercel --prod*", "fly deploy*"], mentions: [deploy] }
179
+ ```
180
+
181
+ The MCP side's `deny` and `approval.tools` patterns also apply to MCP tools the coding agent calls
182
+ directly (`mcp__crm__crm_delete_contact` matches `crm_delete_*`). The kill switch stops every
183
+ guarded call in both modes.
184
+
185
+ **Irreversible classes.** `git-push` (`git push`, not `--dry-run`); `merge` (`gh pr merge`,
186
+ `glab mr merge`, `gh api -X PUT …/merge`); `tag-delete` (`git tag -d`, `gh release delete`);
187
+ `rm-outside-workspace` (`rm`/`rmdir`/`unlink`/`shred`/`find -delete` on anything outside the
188
+ project, the project itself, or — recursive only — a target that depends on a variable; temp
189
+ directories are free); `publish` (`npm`/`pnpm`/`yarn`/`bun publish`, `cargo publish`,
190
+ `twine upload`, `gem push`, `poetry`/`uv publish`, `docker push`, `gh release create`,
191
+ `changeset`/`lerna publish`, `semantic-release`, …); `send` (`mail`/`sendmail`/`mutt`/…,
192
+ `gh pr|issue create/comment/edit/…`, `gh api` writes, `curl` to Slack/Discord/Telegram/mail-API
193
+ hosts, MCP tools named `send`/`reply`/`forward`/`post`); `guard-config` (edits to `agentguard.yaml`
194
+ or a hook settings file, `agentguard hooks …`).
195
+
196
+ **The human-turn gate.** An irreversible command runs when the latest entry a person wrote in the
197
+ transcript (1) came after this session's previous irreversible command — one turn authorizes one
198
+ — (2) is at most `window_s` old, and (3) names the action ("push it"), or is a bare "yes" / "go
199
+ ahead" answering an assistant question that names it ("Want me to push to main?"). Otherwise the
200
+ agent gets `APPROVAL_REQUIRED` with an id, and either the person says so in their own words or runs
201
+ `agentguard approve <id>` (in Claude Code, `! npx @agentwares/agentguard approve <id>` runs it
202
+ without going through the model); the identical retry then runs once.
203
+
204
+ **How "a person wrote it" is decided** (Claude Code transcripts, checked against versions 2.1.170
205
+ to 2.1.286). An entry counts only if: `type` is `user` and `message.role` is `user`; it is not
206
+ `isSidechain` (a subagent's "user" turn is the parent model's prompt); not `isMeta` (skill bodies,
207
+ reminders, peer messages); not a compaction summary; carries no `tool_result` and no
208
+ `toolUseResult` (tool output rides in user entries); if it has an `origin`, `origin.kind` is
209
+ `human` (`task-notification`, `peer`, `coordinator` are not); otherwise, if it has a `turnOrigin`,
210
+ that is `human`; with neither label (older versions, queued prompts, `claude -p`), its text does
211
+ not start with a harness wrapper (`<command-name>`, `<local-command-stdout>`, `<task-notification>`,
212
+ `<bash-input>`, `[Request interrupted`, …), and `require_origin: true` refuses it outright. Text
213
+ inside an assistant entry never counts — that is where a fabricated "user: yes, push it" lives.
214
+
215
+ **What the agent sees.** A denial is Claude Code's / Codex's `permissionDecision: "deny"` (Gemini:
216
+ `decision: "deny"`) whose reason is the usual agentguard body:
217
+
218
+ ```json
219
+ {
220
+ "code": "APPROVAL_REQUIRED",
221
+ "cause": "\"git push origin main\" is irreversible (git-push (git push origin main)) and the latest message typed by a person (08:29:16) does not name this action (expected one of: push, ship). It needs a person's approval (approval apr_7c62fbad56).",
222
+ "fix": "stop and tell the user: this needs their approval. They can run `npx @agentwares/agentguard approve apr_7c62fbad56` in a terminal in this project (or type `! npx @agentwares/agentguard approve apr_7c62fbad56` in Claude Code), or say in their own words that you should do it; then retry this exact command once. Do not change it, work around it, or run the approval yourself: agentguard refuses an approval from the agent.",
223
+ "retryable": true,
224
+ "details": { "approvalId": "apr_7c62fbad56", "command": "agentguard approve apr_7c62fbad56" }
225
+ }
226
+ ```
227
+
228
+ and the person gets a one-line `systemMessage` with the approve command. agentguard never answers
229
+ "allow": an allowed call goes through the agent's own permission prompts as before. The agent may
230
+ not approve, deny or resume its own held actions, or write `.agentguard/` (refused in enforce).
231
+ In dry-run the person sees "would have stopped …" and the call runs; `agentguard report` lists every
232
+ such decision under **Coding-agent hooks**. Hook calls count against `caps` under `hook_calls`,
233
+ `shell_commands`, `file_edits` and `irreversible` (per run = per agent session, and per day); the
234
+ loop breaker stops the same file edit repeated.
235
+
236
+ ## Audit: which written rules your coding agents broke
237
+
238
+ Teams write rules for their agents in CLAUDE.md, AGENTS.md, GEMINI.md and `.cursor/rules`, then
239
+ cannot tell whether the agents keep them. `agentguard audit` reads the repository's rule files and
240
+ this machine's past Claude Code and Codex sessions in it, and prints, per rule, how many sessions
241
+ broke it and when. Measure first, then enforce what can be enforced.
242
+
243
+ ```sh
244
+ npx -y @agentwares/agentguard audit # this repository, the last 7 days
245
+ npx -y @agentwares/agentguard audit --since 30d # or all, 24h, 2026-09-01; --client claude|codex
246
+ npx -y @agentwares/agentguard audit --json # counts, rule ids and dates (what /agentguard:audit reads)
247
+ npx -y @agentwares/agentguard audit --enforce --preview # the hook rules that would enforce them
248
+ ```
249
+
250
+ ```
251
+ agentguard audit — /home/dev/acme · since 2026-09-30
252
+ 6 sessions in this repository (Claude Code 5, Codex 1). Read on this machine; nothing was sent anywhere.
253
+ 13 rules found (6 in CLAUDE.md, 3 in AGENTS.md, 1 in GEMINI.md, 2 in .cursor/rules/release.mdc, 1 in agentguard.yaml); 11 checkable.
254
+
255
+ BROKEN
256
+ R1 Never push to `main`. (CLAUDE.md:5)
257
+ no-push main — broken in 3 of 6 sessions (4 pushed); 1 refused by the harness
258
+ 2026-10-06 10:40 git push origin main · session 3f9a1c2b
259
+ R4 Run `pnpm check` before pushing. (CLAUDE.md:8)
260
+ run "pnpm check" before push — broken in 3 of 6 sessions (4 pushed or opened a PR); 4 times
261
+ …
262
+ NOT CHECKABLE MECHANICALLY (2) — add an inline check (<!-- agentguard: … -->) or ask your agent with /agentguard:audit
263
+ R6 Write tests for new code. (CLAUDE.md:13)
264
+ ```
265
+
266
+ **The rules.** Every list item and every sentence that directs something (code blocks, tables and
267
+ front matter skipped) is a rule, numbered across the files, checkable or not. A small pattern
268
+ library, with no LLM, recognises the common checkable shapes: never push to / commit on / touch
269
+ main directly; never force-push (or "rewrite history"); never `--no-verify` / skip the hooks; run
270
+ `X` before committing or pushing (also tests, lint, typecheck); use pnpm, not npm; do not edit or
271
+ commit `<path>` (`.env`, lockfiles, generated directories); do not add dependencies (without
272
+ asking); never `rm -rf` outside the repo; never publish; never deploy; never use / run `<command>`.
273
+ "unless asked" / "without asking" makes a break on a turn a person typed asking for it not count.
274
+ Two explicit forms always work and win over the patterns:
275
+
276
+ ```markdown
277
+ - Release notes go in CHANGELOG.md. <!-- agentguard: never "git tag*" -->
278
+ - Keep functions small. <!-- agentguard: none -->
279
+ ```
280
+
281
+ ```yaml
282
+ # agentguard.yaml
283
+ audit:
284
+ rules:
285
+ - id: deploys
286
+ text: Only CI deploys
287
+ check: no-deploy # several: check: [no-verify, 'never "git reset --hard*"']
288
+ ```
289
+
290
+ The check grammar: `no-push [branch,…]`, `no-commit-to [branch,…]`, `no-force-push [branch,…]`,
291
+ `no-verify`, `run "<command>" before commit|push` (`@tests`, `@lint`, `@typecheck` for the usual
292
+ commands), `use pnpm|npm|yarn|bun`, `no-pm npm,…`, `no-edit "<glob>" …`, `no-commit "<glob>" …`,
293
+ `no-new-deps`, `no-rm-outside`, `no-publish`, `no-deploy`, `never "<command glob>" …`, each with an
294
+ optional `unless-asked`; `none` marks a rule no script can check.
295
+
296
+ **The sessions.** Claude Code transcripts (`~/.claude/projects`, or `$CLAUDE_CONFIG_DIR`) and
297
+ Codex rollouts (`~/.codex/sessions`, or `$CODEX_HOME`) whose working directory is this repository
298
+ or one of its worktrees, subagents included. The tool calls are read the way hook mode reads them
299
+ (the same shell parser, the same edit targets, the same rm-outside-workspace and publish classes),
300
+ and "asked for it" is hook mode's human-turn reading of the transcript. A push's branch is the one
301
+ on the command line; for a bare `git push`, the one Claude Code recorded the push went to, else
302
+ the branch checked out.
303
+
304
+ **What leaves the screen: nothing.** No network, no telemetry, no LLM. The command or path that
305
+ broke a rule is shown on your terminal only. `--json` and the cache
306
+ (`~/.agentguard/audit-cache-v1/`, counts per transcript so a second run takes about a second and
307
+ history survives Claude Code's 30-day transcript cleanup) hold counts, rule ids, dates, line
308
+ numbers and hashes — never a command, a path or a prompt; a test plants marker strings in every
309
+ part of a synthetic transcript and checks that none reaches either.
310
+
311
+ **`--enforce`.** Writes the checkable rules into agentguard.yaml as hook-mode rules
312
+ (`hooks.shell.deny`, `hooks.shell.approval` for "without asking", `hooks.paths.deny`, the
313
+ built-in irreversible classes, a `deploy` rule), each with a comment naming the rule it came
314
+ from; shows the diff; installs the hook if it is not installed. It never switches `hooks.mode`:
315
+ a new hooks block starts in dry-run, and if yours is already `enforce` it writes nothing without
316
+ `--yes`. "Run X before pushing" and "never commit on main" stay audit-only (a pre-execution hook
317
+ sees one command, not the order or the checked-out branch).
318
+
319
+ **In your agent.** `/agentguard:audit` (Claude Code plugin, Gemini CLI extension) runs
320
+ `audit --json` and has your own agent judge the rules marked not checkable against the
321
+ repository and its git history — no tokens of ours. `/agentguard:hooks` installs or checks hook
322
+ mode.
323
+
124
324
  ## Policy
125
325
 
126
326
  `agentguard init` generates this file with every knob explained inline. The short form:
@@ -189,20 +389,24 @@ Run identity: `X-Run-Id` header (HTTP) → `_meta.runId` on the call → session
189
389
 
190
390
  ## Commands
191
391
 
192
- | Command | What it does |
193
- | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
194
- | `agentguard init [--client path] [--all] [--no-probe] [--mode enforce] [--undo]` | generate the policy, rewrite the client config (project-level by default) |
195
- | `agentguard proxy [--http --port 8788] [--agent name] [--run-id id] [--mode m]` | run the proxy (stdio default) |
196
- | `agentguard report [--run id \| --all] [--json]` | what this run did / would have destroyed / spent; where it was halted; chain status |
197
- | `agentguard diff [--run id]` | mutation diff of faked writes |
198
- | `agentguard verify [audit.jsonl]` | recompute the hash chain; exit 1 on the first break |
199
- | `agentguard status [--run id]` | counters vs caps, kill state, pending approvals, running HTTP proxy |
200
- | `agentguard tools [--json]` | every exposed tool with class, verb, upstream and the reason |
201
- | `agentguard kill [reason]` / `agentguard resume` | halt everything now / clear it |
202
- | `agentguard approvals [--all]` / `approve <id>` / `deny <id> [--note …]` | the approval queue |
203
- | `agentguard key create <agent> [--allow p]… [--deny p] [--writes n] [--spend n] [--mode m]` / `key list` / `key revoke <agent>` | scoped credentials |
204
- | `agentguard connect <key> [--write] [--client path] [--all] [--url base]` | point this machine's MCP client at a hosted proxy (paid tiers); prints the config, `--write` merges it in |
205
- | `agentguard permission-diff [--base ref] [--head ref] [--fail-on-widen]` | which config changes widen agent permissions (also a [GitHub Action](https://github.com/agentwares/agentguard/tree/main/permission-diff)) |
392
+ | Command | What it does |
393
+ | ----------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
394
+ | `agentguard init [--client path] [--all] [--no-probe] [--mode enforce] [--undo]` | generate the policy, rewrite the client config (project-level by default) |
395
+ | `agentguard proxy [--http --port 8788] [--agent name] [--run-id id] [--mode m]` | run the proxy (stdio default) |
396
+ | `agentguard report [--run id \| --all] [--json]` | what this run did / would have destroyed / spent; where it was halted; chain status |
397
+ | `agentguard diff [--run id]` | mutation diff of faked writes |
398
+ | `agentguard verify [audit.jsonl]` | recompute the hash chain; exit 1 on the first break |
399
+ | `agentguard status [--run id]` | counters vs caps, kill state, pending approvals, running HTTP proxy |
400
+ | `agentguard tools [--json]` | every exposed tool with class, verb, upstream and the reason |
401
+ | `agentguard kill [reason]` / `agentguard resume` | halt everything now / clear it |
402
+ | `agentguard approvals [--all]` / `approve <id>` / `deny <id> [--note …]` | the approval queue |
403
+ | `agentguard key create <agent> [--allow p]… [--deny p] [--writes n] [--spend n] [--mode m]` / `key list` / `key revoke <agent>` | scoped credentials |
404
+ | `agentguard connect <key> [--write] [--client path] [--all] [--url base]` | point this machine's MCP client at a hosted proxy (paid tiers); prints the config, `--write` merges it in |
405
+ | `agentguard permission-diff [--base ref] [--head ref] [--fail-on-widen]` | which config changes widen agent permissions (also a [GitHub Action](https://github.com/agentwares/agentguard/tree/main/permission-diff)) |
406
+ | `agentguard hooks install [--project\|--user] [--client claude\|codex\|gemini\|all] [--command cmd]` / `hooks uninstall` / `hooks status` | put the policy in front of a coding agent's shell commands and file edits ([hook mode](#hook-mode-a-coding-agents-own-commands)) |
407
+ | `agentguard hook pre-tool-use [--client c]` | what the installed hook runs: reads the harness's JSON on stdin, answers in its format |
408
+ | `agentguard audit [--since 7d\|30d\|all] [--client claude\|codex\|all] [--json] [--rules file] [--no-cache] [--no-evidence]` | which written rules the coding agents broke, per rule, from this machine's sessions ([audit](#audit-which-written-rules-your-coding-agents-broke)) |
409
+ | `agentguard audit --enforce [--preview] [--yes]` | write the checkable rules into agentguard.yaml as hook rules (dry-run), show the diff, install the hook |
206
410
 
207
411
  ### Hosted tiers
208
412
 
@@ -239,11 +443,14 @@ node dist/cli.js report && node dist/cli.js diff && node dist/cli.js verify
239
443
 
240
444
  ## Conformance and tests
241
445
 
242
- `pnpm test` runs the CLI suite (52 tests; 68 more in `agentguard-core`, 11 in the SDK): the engine over InMemoryTransport, the spawned stdio proxy (with and without a policy file), the Streamable HTTP proxy with `X-Run-Id`, scoped keys and control endpoints, `init` against real configs, and a recorded-fixture replay (`fixtures/recorded/crm-session.json`; re-record with `RECORD_FIXTURES=1`). `pnpm conformance` runs the official `@modelcontextprotocol/conformance` server suite against the proxy with a sample server behind it (tools, resources, prompts, completions, logging, progress, sampling and elicitation are relayed).
446
+ `pnpm test` runs the CLI suite (85 tests; 191 more in `agentguard-core`, 11 in the SDK): the audit end to end on a synthetic history (rule files in every format, Claude Code and Codex sessions with known kept and broken rules, marker strings that must never reach the cache or `--json`, `--enforce`), hook mode end to end (install for three clients, each client's answer format, the approve flow, synthetic transcripts reproducing a model-written "user: yes, push it"), the engine over InMemoryTransport, the spawned stdio proxy (with and without a policy file), the Streamable HTTP proxy with `X-Run-Id`, scoped keys and control endpoints, `init` against real configs, and a recorded-fixture replay (`fixtures/recorded/crm-session.json`; re-record with `RECORD_FIXTURES=1`). `pnpm conformance` runs the official `@modelcontextprotocol/conformance` server suite against the proxy with a sample server behind it (tools, resources, prompts, completions, logging, progress, sampling and elicitation are relayed).
243
447
 
244
448
  ## Limits (honest)
245
449
 
246
- - The proxy sees MCP tool calls. Token spend on the model API is only visible through the SDK's guarded `fetch` (or `spend.tools` rules for MCP tools that call models).
450
+ - The proxy sees MCP tool calls; hook mode sees a coding agent's shell commands, file edits and direct MCP calls. Token spend on the model API is only visible through the SDK's guarded `fetch` (or `spend.tools` rules for MCP tools that call models).
451
+ - Hook mode is a guardrail against an agent acting without a person's say-so, not a sandbox against a hostile one. It reads command lines, not what runs: a script (`./release.sh`, `make deploy`) that pushes inside is only caught by a `shell` rule naming it; `xargs rm` targets come from stdin and are not checked; a variable's value is unknown (a recursive `rm` of one counts as outside the workspace). "Names the action" is a word match: "don't push yet" names push. Each class's words are in the policy block; add your language's with `irreversible.mentions`.
452
+ - Claude Code writes the transcript asynchronously; if the turn that authorizes a command is not on disk yet when the hook runs, the command is held (fail closed) and the retry passes. A `claude -p` prompt has no `origin` label, so it counts as the person's turn unless `require_origin: true`. Codex and Gemini CLI get no human-turn gate (their irreversible commands always need `agentguard approve` in enforce). If the hook itself fails (a broken `agentguard.yaml`), the call runs and the person sees "this call was NOT checked".
453
+ - The audit counts what the transcripts show. Rules are recognised by patterns, not understood: a rule worded unusually is listed as not checkable (add an inline check), and one worded like a pattern it does not mean can be mis-read (mark it `<!-- agentguard: none -->`). A bare `git push` counts as a push to the checked-out branch only on the main thread (a subagent's `gitBranch` can be the session's, not its worktree's); an attempt the harness refused still counts, marked as refused. Sessions are matched by working directory: a worktree outside the repository is not included. Codex rollouts do not say which turns a person typed, so an `unless-asked` break there is reported as undetermined; Codex's JavaScript `exec` tool is counted, not read.
247
454
  - Dry-run synthesizes results from the tool's `outputSchema`; agents that depend on real ids from a create → update chain will see plausible but fake ids. `dry_run.tools` lets you fake only the dangerous tools in enforce mode.
248
455
  - Per-day counters are a JSON file under a directory lock; fine for a workstation or one box, not a fleet. The hosted tier (coming) is the shared-state version.
249
456
  - Slack "Approve" buttons are links to the local HTTP proxy; they work for people who can reach it. Without HTTP mode the message carries the `agentguard approve <id>` command.
@@ -0,0 +1,18 @@
1
+ import {
2
+ TEAM_HISTORY_LINE,
3
+ TEAM_HISTORY_URL,
4
+ auditCommand,
5
+ parseSince
6
+ } from "./chunk-YBVD6HTT.js";
7
+ import "./chunk-F73VDY3W.js";
8
+ import "./chunk-2QDPPTPY.js";
9
+ import "./chunk-XPNBYSIN.js";
10
+ import "./chunk-4WTFMFRY.js";
11
+ import "./chunk-5WRI5ZAA.js";
12
+ export {
13
+ TEAM_HISTORY_LINE,
14
+ TEAM_HISTORY_URL,
15
+ auditCommand,
16
+ parseSince
17
+ };
18
+ //# sourceMappingURL=audit-5LUDXZLG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,179 @@
1
+ // src/hooks/clients.ts
2
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
3
+ import { dirname, join } from "path";
4
+ import {
5
+ LEGACY_POLICY_FILE,
6
+ LEGACY_STATE_DIR,
7
+ POLICY_FILE,
8
+ STATE_DIR
9
+ } from "@agentwares/agentguard-core";
10
+ var MATCHERS = {
11
+ "claude-code": "^(Bash|PowerShell|Write|Edit|MultiEdit|NotebookEdit)$|^mcp__",
12
+ codex: "^(Bash|apply_patch)$|^mcp__",
13
+ "gemini-cli": "^(run_shell_command|write_file|replace)$|^mcp_"
14
+ };
15
+ var CLIENT_ALIASES = {
16
+ claude: "claude-code",
17
+ "claude-code": "claude-code",
18
+ codex: "codex",
19
+ gemini: "gemini-cli",
20
+ "gemini-cli": "gemini-cli"
21
+ };
22
+ function hookTarget(client, scope, where, command, timeoutS = 30) {
23
+ const root = scope === "project" ? where.cwd : where.home;
24
+ if (client === "claude-code")
25
+ return {
26
+ client,
27
+ label: "Claude Code",
28
+ scope,
29
+ settingsPath: join(root, ".claude", "settings.json"),
30
+ event: "PreToolUse",
31
+ matcher: MATCHERS[client],
32
+ handler: { type: "command", command, timeout: timeoutS, statusMessage: "okgate" }
33
+ };
34
+ if (client === "codex")
35
+ return {
36
+ client,
37
+ label: "Codex CLI",
38
+ scope,
39
+ settingsPath: join(root, ".codex", "hooks.json"),
40
+ event: "PreToolUse",
41
+ matcher: MATCHERS[client],
42
+ handler: { type: "command", command, timeout: timeoutS, statusMessage: "okgate" },
43
+ note: "Codex runs a new hook only after you trust it: open /hooks in Codex and trust the okgate entry."
44
+ };
45
+ return {
46
+ client,
47
+ label: "Gemini CLI",
48
+ scope,
49
+ settingsPath: join(root, ".gemini", "settings.json"),
50
+ event: "BeforeTool",
51
+ matcher: MATCHERS[client],
52
+ handler: {
53
+ name: "okgate",
54
+ type: "command",
55
+ command,
56
+ timeout: timeoutS * 1e3,
57
+ description: "okgate: one rulebook for shell commands and file edits"
58
+ },
59
+ note: scope === "project" ? "Gemini CLI fingerprints project hooks: confirm the okgate hook the first time it asks." : void 0
60
+ };
61
+ }
62
+ function isGuardHookCommand(command) {
63
+ return typeof command === "string" && /okgate|agentguard/.test(command) && /\bhook\s+pre-tool-use\b/.test(command);
64
+ }
65
+ function backupFor(path) {
66
+ const legacy = `${path}.agentguard-backup`;
67
+ const next = `${path}.okgate-backup`;
68
+ return { existing: [legacy, next].find((b) => existsSync(b)), next };
69
+ }
70
+ function readSettings(path) {
71
+ if (!existsSync(path)) return {};
72
+ const text = readFileSync(path, "utf8");
73
+ if (!text.trim()) return {};
74
+ let parsed;
75
+ try {
76
+ parsed = JSON.parse(text);
77
+ } catch (err) {
78
+ throw new Error(
79
+ `${path} is not valid JSON (${err instanceof Error ? err.message : String(err)}); fix it or move it aside \u2014 okgate will not overwrite it`,
80
+ { cause: err }
81
+ );
82
+ }
83
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
84
+ throw new Error(`${path} is not a JSON object; okgate will not overwrite it`);
85
+ return parsed;
86
+ }
87
+ function groupsOf(settings, event) {
88
+ const hooks = settings.hooks;
89
+ if (!hooks || typeof hooks !== "object") return [];
90
+ const groups = hooks[event];
91
+ return Array.isArray(groups) ? groups : [];
92
+ }
93
+ function installedHandlers(target) {
94
+ const settings = readSettings(target.settingsPath);
95
+ return groupsOf(settings, target.event).flatMap(
96
+ (g) => (g.hooks ?? []).filter((h) => isGuardHookCommand(h.command))
97
+ );
98
+ }
99
+ function installHook(target) {
100
+ const existed = existsSync(target.settingsPath);
101
+ const settings = readSettings(target.settingsPath);
102
+ const hooks = settings.hooks && typeof settings.hooks === "object" ? settings.hooks : {};
103
+ const groups = groupsOf(settings, target.event);
104
+ const isOurs = (h) => isGuardHookCommand(h.command);
105
+ const ours = groups.flatMap((g) => (g.hooks ?? []).filter(isOurs));
106
+ const exact = groups.some(
107
+ (g) => g.matcher === target.matcher && g.hooks?.length === 1 && JSON.stringify(g.hooks[0]) === JSON.stringify(target.handler)
108
+ );
109
+ if (exact && ours.length === 1) return { status: "unchanged" };
110
+ const kept = [];
111
+ for (const group of groups) {
112
+ if (!(group.hooks ?? []).some(isOurs)) {
113
+ kept.push(group);
114
+ continue;
115
+ }
116
+ const others = (group.hooks ?? []).filter((h) => !isOurs(h));
117
+ if (others.length > 0) kept.push({ ...group, hooks: others });
118
+ }
119
+ kept.push({ matcher: target.matcher, hooks: [target.handler] });
120
+ const status = ours.length > 0 ? "updated" : "added";
121
+ hooks[target.event] = kept;
122
+ settings.hooks = hooks;
123
+ let backup;
124
+ if (existed) {
125
+ const { existing, next } = backupFor(target.settingsPath);
126
+ backup = existing ?? next;
127
+ if (!existing) copyFileSync(target.settingsPath, backup);
128
+ }
129
+ mkdirSync(dirname(target.settingsPath), { recursive: true });
130
+ writeFileSync(target.settingsPath, JSON.stringify(settings, null, 2) + "\n");
131
+ return { status, backup };
132
+ }
133
+ function uninstallHook(target) {
134
+ if (!existsSync(target.settingsPath)) return 0;
135
+ const settings = readSettings(target.settingsPath);
136
+ const groups = groupsOf(settings, target.event);
137
+ let removed = 0;
138
+ const kept = [];
139
+ for (const group of groups) {
140
+ const others = (group.hooks ?? []).filter((h) => !isGuardHookCommand(h.command));
141
+ removed += (group.hooks ?? []).length - others.length;
142
+ if (others.length > 0) kept.push({ ...group, hooks: others });
143
+ }
144
+ if (removed === 0) return 0;
145
+ const hooks = settings.hooks;
146
+ if (kept.length > 0) hooks[target.event] = kept;
147
+ else delete hooks[target.event];
148
+ if (Object.keys(hooks).length === 0) delete settings.hooks;
149
+ writeFileSync(target.settingsPath, JSON.stringify(settings, null, 2) + "\n");
150
+ return removed;
151
+ }
152
+ function hookSettingsFiles(where) {
153
+ const out = [];
154
+ for (const root of [where.cwd, where.home]) {
155
+ out.push(
156
+ join(root, ".claude", "settings.json"),
157
+ join(root, ".claude", "settings.local.json"),
158
+ join(root, ".codex", "hooks.json"),
159
+ join(root, ".codex", "config.toml"),
160
+ join(root, ".gemini", "settings.json")
161
+ );
162
+ }
163
+ out.push(
164
+ join(where.home, STATE_DIR, POLICY_FILE),
165
+ join(where.home, LEGACY_STATE_DIR, LEGACY_POLICY_FILE)
166
+ );
167
+ return out;
168
+ }
169
+
170
+ export {
171
+ CLIENT_ALIASES,
172
+ hookTarget,
173
+ backupFor,
174
+ installedHandlers,
175
+ installHook,
176
+ uninstallHook,
177
+ hookSettingsFiles
178
+ };
179
+ //# sourceMappingURL=chunk-2QDPPTPY.js.map