Guide ·

What a dry-run of an agent's MCP writes shows

okgate is an MCP proxy: it sits between an agent and the MCP servers it calls. In dry-run mode it passes the agent’s reads through to the real servers and fakes its writes. The agent gets back a plausible success, nothing reaches the server, and the call is written to an audit log. After the run, okgate report says what the run would have done and okgate diff lists every write it faked.

Everything below is the real output of one run on 7 October 2026, made with agentguard 0.1.6 (okgate’s name until 8 October 2026; the same commands now print okgate and write okgate.yaml and .okgate/) and the two fixtures that ship with it: a fake CRM with ten tools and a scripted agent that misbehaves on purpose.

1. init writes a dry-run policy

okgate init finds the project’s MCP config (.mcp.json, .cursor/mcp.json, .vscode/mcp.json, mcp.json or .gemini/settings.json), connects to each server it names and lists its tools, and writes okgate.yaml with mode: dry-run. It then points the config at the proxy and keeps a backup; okgate init --undo puts the old one back.

$ npx @agentwares/agentguard init
found   .mcp.json (Claude Code (project .mcp.json)) → 1 server: crm
probing 1 upstream…
        crm: 10 tools
wrote   agentguard.yaml (mode: dry-run, 1 upstream, 10 tools: 4 read, 4 write, 1 spend, 1 unknown)
rewrote .mcp.json → crm now go through `npx @agentwares/agentguard proxy` (backup: .mcp.json.agentguard-backup)

Every tool is classified as a read, a write or a spend: first by any pattern you list under classify, then by the tool’s MCP annotations (readOnlyHint, destructiveHint), then by the verb in its name. A tool none of those classify counts as a write. This is what init wrote for the demo CRM:

# Detected tools (10: 4 read, 4 write, 1 spend, 1 unknown):
#   crm_list_contacts                    read     read     annotation: readOnlyHint: true
#   crm_get_contact                      read     read     annotation: readOnlyHint: true
#   crm_search                           read     read     heuristic: verb "search"
#   crm_create_contact                   write    create   annotation: readOnlyHint: false
#   crm_update_contact                   write    update   annotation: readOnlyHint: false
#   crm_delete_contact                   write    delete   annotation: readOnlyHint: false, destructiveHint: true
#   crm_send_email                       write    send     heuristic: verb "send"
#   crm_charge_card                      spend    spend    heuristic: verb "charge"
#   crm_frobnicate                       unknown  unknown  none: no recognizable verb
#   crm_fail                             read     read     annotation: readOnlyHint: true
# readOnlyHint annotations and verb heuristics classify most tools. List anything that is wrong here;
# these patterns win over annotations. `*` matches anything.
classify:
  read: []
  write: ["crm_frobnicate"]   # could not be classified — treated as writes; move to read: if they only read
  spend: ["crm_charge_card"]
  unknown: write   # a tool nobody can classify counts as a write (alternatives: read | block)

2. The agent runs, and its writes are faked

The scripted agent lists contacts, reads one, creates one, updates one, deletes one, sends an email and charges a card $12. Then it repeats the same update, and then it creates contacts in a burst of up to 60. It made 54 calls: the two reads went to the CRM, 50 calls were faked (the charge among them), and two were refused.

Two limits still applied in dry-run, because caps and the loop breaker run in both modes. The third identical update was refused with LOOP_DETECTED, and the burst stopped at the 51st write, over the writes: 50 per-run cap init sets, with CAP_EXCEEDED. The agent receives each refusal as a tool result carrying a code, a cause and a fix.

3. okgate report: what the run would have done

$ agentguard report
agentguard report — run run_dryrun_demo

54 tool calls between 2026-10-07T05:24:00.526Z and 2026-10-07T05:24:00.627Z across crm.

What this run would have done (dry-run, nothing was executed)

It would have deleted 1 record, updated 3, created 44, sent 1 message.

Run agentguard diff for the record-by-record mutation diff. Flip mode: enforce when it looks right.

Where agentguard stepped in
#whencodetoolwhy
102026-10-07T05:24:00.567ZLOOP_DETECTEDcrm_update_contact"crm_update_contact" was called 3 times with the same arguments in the last 30 calls
542026-10-07T05:24:00.627ZCAP_EXCEEDEDcrm_create_contactwrites cap for this run is 50; used 50, this call would make it 51
Calls

By class: write 51, read 2, spend 1By outcome: faked 50, ok 2, halted 1, blocked 1

toolclasscalls
crm_create_contactwrite45
crm_update_contactwrite4
crm_list_contactsread1
crm_get_contactread1
crm_delete_contactwrite1
crm_send_emailwrite1
crm_charge_cardspend1

Audit chain: 54 entries, verified (~/demo/.agentguard/audit.jsonl).

The report names no dollar amount. crm_charge_card was classified as a spend from the verb in its name, but the policy init writes has no rule for reading an amount from that tool’s arguments, so the charge counted as $0 (default_usd: 0). One line of policy fixes that; it is in spend caps for the charges an agent’s tools make.

4. okgate diff: every faked write, with its arguments

One entry per faked call, in the order the agent made them, with the arguments it sent: +++ create, ~~~ update, --- delete, >>> send, $$$ spend. The bracket gives the call’s number in the audit log and its time.

$ okgate diff+++ CREATE via crm/crm_create_contact (name=Demo Person)  [#3 2026-10-07T05:24:00.551Z]
+ {
+   "name": "Demo Person",
+   "email": "demo@example.com"
+ }~~~ UPDATE via crm/crm_update_contact (id=c_1)  [#4 2026-10-07T05:24:00.555Z]
~ {
~   "id": "c_1",
~   "fields": {
~     "name": "Ada L."
~   }
~ }--- DELETE via crm/crm_delete_contact (id=c_2)  [#5 2026-10-07T05:24:00.557Z]
- {
-   "id": "c_2"
- }>>> SEND via crm/crm_send_email (to=ada@example.com)  [#6 2026-10-07T05:24:00.559Z]
> {
>   "to": "ada@example.com",
>   "subject": "Hi",
>   "body": "Hello from the demo agent"
> }$$$ SPEND via crm/crm_charge_card (customer_id=c_1)  [#7 2026-10-07T05:24:00.561Z]
$ {
$   "customer_id": "c_1",
$   "amount_cents": 1200,
$   "currency": "usd"
$ }~~~ UPDATE via crm/crm_update_contact (id=c_3)  [#8 2026-10-07T05:24:00.563Z]
~ {
~   "id": "c_3",
~   "fields": {
~     "name": "Same Thing"
~   }
~ }~~~ UPDATE via crm/crm_update_contact (id=c_3)  [#9 2026-10-07T05:24:00.565Z]
~ {
~   "id": "c_3",
~   "fields": {
~     "name": "Same Thing"
~   }
~ }
The other 43 entries
+++ CREATE via crm/crm_create_contact (name=Bulk 0)  [#11 2026-10-07T05:24:00.568Z]
+ {
+   "name": "Bulk 0",
+   "email": "bulk0@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 1)  [#12 2026-10-07T05:24:00.569Z]
+ {
+   "name": "Bulk 1",
+   "email": "bulk1@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 2)  [#13 2026-10-07T05:24:00.571Z]
+ {
+   "name": "Bulk 2",
+   "email": "bulk2@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 3)  [#14 2026-10-07T05:24:00.572Z]
+ {
+   "name": "Bulk 3",
+   "email": "bulk3@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 4)  [#15 2026-10-07T05:24:00.574Z]
+ {
+   "name": "Bulk 4",
+   "email": "bulk4@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 5)  [#16 2026-10-07T05:24:00.575Z]
+ {
+   "name": "Bulk 5",
+   "email": "bulk5@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 6)  [#17 2026-10-07T05:24:00.576Z]
+ {
+   "name": "Bulk 6",
+   "email": "bulk6@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 7)  [#18 2026-10-07T05:24:00.578Z]
+ {
+   "name": "Bulk 7",
+   "email": "bulk7@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 8)  [#19 2026-10-07T05:24:00.580Z]
+ {
+   "name": "Bulk 8",
+   "email": "bulk8@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 9)  [#20 2026-10-07T05:24:00.581Z]
+ {
+   "name": "Bulk 9",
+   "email": "bulk9@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 10)  [#21 2026-10-07T05:24:00.583Z]
+ {
+   "name": "Bulk 10",
+   "email": "bulk10@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 11)  [#22 2026-10-07T05:24:00.584Z]
+ {
+   "name": "Bulk 11",
+   "email": "bulk11@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 12)  [#23 2026-10-07T05:24:00.585Z]
+ {
+   "name": "Bulk 12",
+   "email": "bulk12@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 13)  [#24 2026-10-07T05:24:00.586Z]
+ {
+   "name": "Bulk 13",
+   "email": "bulk13@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 14)  [#25 2026-10-07T05:24:00.587Z]
+ {
+   "name": "Bulk 14",
+   "email": "bulk14@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 15)  [#26 2026-10-07T05:24:00.589Z]
+ {
+   "name": "Bulk 15",
+   "email": "bulk15@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 16)  [#27 2026-10-07T05:24:00.590Z]
+ {
+   "name": "Bulk 16",
+   "email": "bulk16@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 17)  [#28 2026-10-07T05:24:00.592Z]
+ {
+   "name": "Bulk 17",
+   "email": "bulk17@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 18)  [#29 2026-10-07T05:24:00.593Z]
+ {
+   "name": "Bulk 18",
+   "email": "bulk18@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 19)  [#30 2026-10-07T05:24:00.594Z]
+ {
+   "name": "Bulk 19",
+   "email": "bulk19@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 20)  [#31 2026-10-07T05:24:00.595Z]
+ {
+   "name": "Bulk 20",
+   "email": "bulk20@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 21)  [#32 2026-10-07T05:24:00.597Z]
+ {
+   "name": "Bulk 21",
+   "email": "bulk21@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 22)  [#33 2026-10-07T05:24:00.599Z]
+ {
+   "name": "Bulk 22",
+   "email": "bulk22@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 23)  [#34 2026-10-07T05:24:00.600Z]
+ {
+   "name": "Bulk 23",
+   "email": "bulk23@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 24)  [#35 2026-10-07T05:24:00.601Z]
+ {
+   "name": "Bulk 24",
+   "email": "bulk24@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 25)  [#36 2026-10-07T05:24:00.603Z]
+ {
+   "name": "Bulk 25",
+   "email": "bulk25@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 26)  [#37 2026-10-07T05:24:00.605Z]
+ {
+   "name": "Bulk 26",
+   "email": "bulk26@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 27)  [#38 2026-10-07T05:24:00.606Z]
+ {
+   "name": "Bulk 27",
+   "email": "bulk27@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 28)  [#39 2026-10-07T05:24:00.607Z]
+ {
+   "name": "Bulk 28",
+   "email": "bulk28@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 29)  [#40 2026-10-07T05:24:00.609Z]
+ {
+   "name": "Bulk 29",
+   "email": "bulk29@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 30)  [#41 2026-10-07T05:24:00.610Z]
+ {
+   "name": "Bulk 30",
+   "email": "bulk30@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 31)  [#42 2026-10-07T05:24:00.611Z]
+ {
+   "name": "Bulk 31",
+   "email": "bulk31@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 32)  [#43 2026-10-07T05:24:00.613Z]
+ {
+   "name": "Bulk 32",
+   "email": "bulk32@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 33)  [#44 2026-10-07T05:24:00.614Z]
+ {
+   "name": "Bulk 33",
+   "email": "bulk33@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 34)  [#45 2026-10-07T05:24:00.615Z]
+ {
+   "name": "Bulk 34",
+   "email": "bulk34@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 35)  [#46 2026-10-07T05:24:00.616Z]
+ {
+   "name": "Bulk 35",
+   "email": "bulk35@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 36)  [#47 2026-10-07T05:24:00.617Z]
+ {
+   "name": "Bulk 36",
+   "email": "bulk36@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 37)  [#48 2026-10-07T05:24:00.619Z]
+ {
+   "name": "Bulk 37",
+   "email": "bulk37@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 38)  [#49 2026-10-07T05:24:00.620Z]
+ {
+   "name": "Bulk 38",
+   "email": "bulk38@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 39)  [#50 2026-10-07T05:24:00.622Z]
+ {
+   "name": "Bulk 39",
+   "email": "bulk39@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 40)  [#51 2026-10-07T05:24:00.623Z]
+ {
+   "name": "Bulk 40",
+   "email": "bulk40@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 41)  [#52 2026-10-07T05:24:00.624Z]
+ {
+   "name": "Bulk 41",
+   "email": "bulk41@example.com"
+ }+++ CREATE via crm/crm_create_contact (name=Bulk 42)  [#53 2026-10-07T05:24:00.626Z]
+ {
+   "name": "Bulk 42",
+   "email": "bulk42@example.com"
+ }
50 mutations would have run: 1 delete, 3 updates, 44 creates, 1 send, 0 executes.

5. The log behind both

Each call is one JSON line in .okgate/audit.jsonl, chained to the one before it by a hash. okgate verify recomputes the chain and fails on the first entry that was edited, removed from the middle or moved:

$ agentguard verify
ok: 54 entries, chain intact, head 4cb1d13f099627b1… (~/demo/.agentguard/audit.jsonl)

Entries cut from the end of the file leave a shorter chain that still verifies; record the head hash and the entry count somewhere else to catch that.

What a dry-run does not show

When the diff looks right, set mode: enforce. Tools listed under dry_run.tools stay faked in enforce mode, so the dangerous ones can stay in dry-run while the rest go live. The live demo runs the same kind of agent through a hosted proxy every 15 minutes.

Try it

In the project whose MCP config your agent uses:

$ npx -p @agentwares/agentguard okgate init