Skip to content

Mark a tool read-only

View as Markdown

This guide marks a tool that only reads, so dry runs run it and approval gates let it through.

  • A tool, in the quickstarts' pack, my-pack, with kindgi dev running and Kindgi 0.2's CLI.

A tool declares whether it changes anything outside Kindgi. One that only reads, such as a lookup, a search or a calculation, says so. Ask your coding agent: "My order lookup tool only reads. Mark it read-only." Kindgi's skills in your pack tell it how (How it knows Kindgi). For reference, the tool's definition then looks like this:

// in tools/lookup-order/index.ts
const defined = defineTool({
id: 'my-pack.lookup-order' as ToolId,
// …
effects: [],
mutating: false,
// …
});
Excerpt

Leaving mutating out is the same as mutating: true. That's the safe default: a tool counts as one that changes things until it says otherwise. It decides two things: whether the tool runs in a dry run, and whether an approval gate asks before it.

--dry-run runs a flow without changing anything. A tool step runs only if the tool is read-only. --flow is the flow, and --input its input:

Terminal window
pnpm exec kindgi runs start --flow=my-pack.check-order --input='{"orderId":"ord_1001"}' --dry-run
"status": "completed",
"dryRun": true,
…
"output": {
"status": "shipped",
"orderId": "ord_1001",
"totalCents": 4200
},
Excerpt

The first tool that isn't stops the run, before the tool is called:

Terminal window
pnpm exec kindgi runs start --flow=my-pack.note-order --input='{"orderId":"ord_1001","text":"Customer called"}' --dry-run
"status": "failed",
"dryRun": true,
"failureMessage": "dry-run-effectful-tool: tool \"my-pack.add-order-note\" is not declared read-only (mutating: false), so it does not run in a dry run",
Excerpt

An agent step in a dry run skips its model call, so the agent calls no tools. The sample's echo-flow greets with a read-only tool, then hands the greeting to its agent:

Terminal window
pnpm exec kindgi runs start --flow=my-pack.echo-flow --input='{"name":"Ada"}' --dry-run
"status": "completed",
"dryRun": true,
…
"output": {
"reply": "[dry-run: model call skipped]",
"greeting": "Hello, Ada!"
},
Excerpt

Use a dry run to check a flow's wiring before it touches real data.

effects lists what a tool touches, for review and policy: { kind: 'writes', resource: 'external:orders' }. The kinds are reads, writes, deletes, network, spawns-run, emits-event, external-side-effect and sensitive-data-egress.

A tool with a writes, deletes, spawns-run, emits-event or external-side-effect effect doesn't run in a dry run even when it says mutating: false. Keep the two consistent: a tool that only reads declares mutating: false and, at most, reads effects.

An agent can make a person approve tool calls, as Ask before a tool runs shows. With its gates on and no rule for a tool, a read-only tool runs without asking. Any other tool waits for approval on its first use.

mutating: false doesn't lift every wait. In a run no person started, a read that uses a person's connection still waits for that person's yes (Use a connection in a tool). In a run a mail started, so does a read that uses a connection of the run's own mailbox, its app and whose it is (What waits for a yes).

A tool whose results are text from outside that nobody checked, such as a mail someone sent or a page a search found, says so: resultIsOutsideData: true (result_is_outside_data=True in Python, .resultIsOutsideData(true) in Java and Scala). An agent then reads each result of the tool as data, not instructions, in that turn and in every later turn's history. Absent means false. The ready-made mail reads set it (Ready-made tools).

It changes what the model is told, not what the tool may do: pair it with approvals for anything an agent could do with what it read.

  • Declare mutating: false on every tool that only reads. Otherwise a dry run stops at it, and an approval gate asks before it.
  • Never declare it on a tool that writes, sends or deletes. A dry run would then run it for real.
  • A tool from an MCP server always counts as one that changes things: it doesn't run in a dry run.