# Mark a tool read-only

> Declare which tools only read, so they run in a dry run and pass approval gates without asking.

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

## Before you start

- **A tool,** in the quickstarts' pack, `my-pack`, with `kindgi dev` running and Kindgi 0.1's CLI.

## Mark it read-only

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](https://docs.kindgi.com/v0.1.8/start/coding-agents.md)). For reference, the tool's definition then looks like this:

- **TypeScript**

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

- **Python**

  ```python excerpt
  # in tools/lookup_order.py
  @tool(id="my-pack.lookup-order", mutating=False)
  def lookup_order(ref: OrderRef, ctx: ToolContext) -> Order:
      ...
  ```

- **Java**

  ```java excerpt
  // in src/main/java/mypack/tools/LookupOrder.java
    public static final Tool<Input, Output> TOOL = Tool.define("my-pack.lookup-order")
        // …
        .mutating(false)
        // …
  ```

- **Scala**

  ```scala excerpt
  // in src/main/scala/mypack/tools/LookupOrder.scala
    val tool: Tool[Input, Output] = Tool[Input, Output]("my-pack.lookup-order")
      // …
      .readOnly
      // …
  ```

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 runs

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

- **TypeScript**

  ```sh
  pnpm exec kindgi runs start --flow=my-pack.check-order --input='{"orderId":"ord_1001"}' --dry-run
  ```

- **Python**

  ```sh
  uv run kindgi runs start --flow=my-pack.check-order --input='{"orderId":"ord_1001"}' --dry-run
  ```

- **Java**

  ```sh
  ./kindgiw runs start --flow=my-pack.check-order --input='{"orderId":"ord_1001"}' --dry-run
  ```

- **Scala**

  ```sh
  ./kindgiw runs start --flow=my-pack.check-order --input='{"orderId":"ord_1001"}' --dry-run
  ```

```json excerpt
  "status": "completed",
  "dryRun": true,
  …
  "output": {
    "status": "shipped",
    "orderId": "ord_1001",
    "totalCents": 4200
  },
```

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

- **TypeScript**

  ```sh
  pnpm exec kindgi runs start --flow=my-pack.note-order --input='{"orderId":"ord_1001","text":"Customer called"}' --dry-run
  ```

- **Python**

  ```sh
  uv run kindgi runs start --flow=my-pack.note-order --input='{"orderId":"ord_1001","text":"Customer called"}' --dry-run
  ```

- **Java**

  ```sh
  ./kindgiw runs start --flow=my-pack.note-order --input='{"orderId":"ord_1001","text":"Customer called"}' --dry-run
  ```

- **Scala**

  ```sh
  ./kindgiw runs start --flow=my-pack.note-order --input='{"orderId":"ord_1001","text":"Customer called"}' --dry-run
  ```

```json excerpt
  "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",
```

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:

- **TypeScript**

  ```sh
  pnpm exec kindgi runs start --flow=my-pack.echo-flow --input='{"name":"Ada"}' --dry-run
  ```

- **Python**

  ```sh
  uv run kindgi runs start --flow=my-pack.echo-flow --input='{"name":"Ada"}' --dry-run
  ```

- **Java**

  ```sh
  ./kindgiw runs start --flow=my-pack.echo-flow --input='{"name":"Ada"}' --dry-run
  ```

- **Scala**

  ```sh
  ./kindgiw runs start --flow=my-pack.echo-flow --input='{"name":"Ada"}' --dry-run
  ```

```json excerpt
  "status": "completed",
  "dryRun": true,
  …
  "output": {
    "reply": "[dry-run: model call skipped]",
    "greeting": "Hello, Ada!"
  },
```

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

## Effects

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

## Approval gates

An agent can make a person approve tool calls, as [Ask before a tool runs](https://docs.kindgi.com/v0.1.8/guides/approvals/ask-before-a-tool-runs.md) 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.

## Getting it right

- 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](https://docs.kindgi.com/v0.1.8/guides/tools/mcp-servers.md) always counts as one that changes things: it doesn't run in a dry run.

## Next

- [Dry-run a flow](https://docs.kindgi.com/v0.1.8/guides/flows/dry-run-a-flow.md)
- [Ask before a tool runs](https://docs.kindgi.com/v0.1.8/guides/approvals/ask-before-a-tool-runs.md)
- [Use an MCP server's tools](https://docs.kindgi.com/v0.1.8/guides/tools/mcp-servers.md)
