# When to add a tool vs. when to write a script

Web page: https://academy.memberpress.ai/courses/code-agents/l6/

Lesson 6 of 10 in [Claude Code Agents — Sub-Agents, Tools, and MCP](https://academy.memberpress.ai/courses/code-agents/), 18 min.

Video: https://www.youtube.com/watch?v=ZqpesvgU4MQ

Every tool you build carries a cost the model pays at runtime: the schema must be accurate, the description must be precise, and the model spends a decision cycle on "should I call this now?" For operations that always run at the same point in a fixed sequence, that cost buys nothing. The operation belongs in a script.

Here is the decision in one sentence: if the model needs to *choose* whether to call the operation — because the choice depends on something it discovers mid-task — make it a tool. If the model just needs to *execute* it at a known point in the sequence, write a script and call it with the Bash tool.

**Three real examples**

A build-and-test script runs after every code change in a fixed pipeline. The model does not decide whether to run it — it always runs at step 3. Script:

```bash
# scripts/build-and-test.sh
npm run build && npm test -- --passWithNoTests
```

A database migration applies once, in a specific order, triggered by a deploy event. The sequence is fixed and the model does not decide when to run it. Script:

```bash
# scripts/migrate.sh
npx prisma migrate deploy
```

A feature flag check is different. The model does not know ahead of time whether a flag is enabled — it needs to check, read the result, and branch based on what it finds. The call depends on a value that only exists during execution. Tool:

```json
{
  "name": "get_feature_flag",
  "description": "Check if a feature flag is enabled for a given environment. Call this before attempting any operation that may be gated by a flag, or when the user asks whether a feature is live.",
  "input_schema": {
    "type": "object",
    "properties": {
      "flag_key": { "type": "string" },
      "environment": { "type": "string", "enum": ["production", "staging", "development"] }
    },
    "required": ["flag_key", "environment"]
  }
}
```

**The quick test**

Ask: could a programmer reading the task description predict exactly when this operation gets called, without running the task? If yes — if the call is predictable from the task spec — it is a script. If no — if the call depends on a value, a state, or a finding that only exists during execution — it is a tool.

A second test: does the model need to do something different based on what the operation returns? If the model acts the same way regardless of the result, it does not need to make a decision — it just needs to run something. That is a script.

**Where this goes wrong**

The most common mistake is building a tool for something that always gets called the same way. The tool adds a description to maintain, a schema to keep accurate, and cognitive weight in the model's decision loop. Three months later, the description is stale, the schema has drifted from the implementation, and the model occasionally decides not to call the tool because the description no longer matches the task. The operation that should always run is sometimes skipped.

The inverse mistake: writing a script for something that requires a decision. The script gets called unconditionally, fails in cases it was not written for, and the model has no way to handle the failure gracefully because the decision point was removed from the model's control. The model cannot branch — it can only run what comes next.

**If you get stuck**

You built a tool and the model calls it too aggressively — every turn, even when the task does not require it. The description is too broad. Narrow it with a negative case: "Call this when X. Do not call this when Y." The negative case is as important as the positive one for tools that are adjacent to other tools in the session.

You have a script, and it fails silently on some inputs. The Bash tool returns the exit code — but only if you check it. Wrap the call: `if ! ./scripts/migrate.sh; then echo "Migration failed, stopping"; exit 1; fi`. A silent failure that lets the pipeline continue is harder to debug than a loud one that stops it. Scripts should fail loudly or not at all.

You are not sure whether something is a tool or a script. Default to script. Build the tool only when you have observed the model needing to make a decision based on the operation's result. Do not speculate about future decision-making needs. Tools added speculatively are maintained indefinitely.

**Cross-reference**

l4 and l5 cover tool schemas and MCP servers — the mechanics of building a tool. This lesson is the complement: it tells you when not to build one. l7 applies both choices to a full PR pipeline where some operations are scripted (build, test, commit) and some require tool calls (branch existence check, PR status).

**Deliverable**

Audit three operations in your current project. For each: state whether it is currently a tool or a script, whether that classification is correct given the decision test above, and one sentence of justification. If one is wrong, fix it. The deliverable is the audit plus one actual change — a tool moved to a script, or a script promoted to a tool — with the reasoning committed to the repo as a comment in the file.

## Try these prompts

- Apply the decision test to these three operations and classify each as tool or script: (1) run npm test after every code change, (2) check whether a feature flag is enabled before rendering a component, (3) apply a database migration during deploy.
- Write a one-sentence negative case to add to a tool description that is being called too aggressively on every turn.
- Write the bash wrapper for a migration script that fails loudly and stops the pipeline if the script exits non-zero.

Next: [A sub-agent that ships PRs](https://academy.memberpress.ai/courses/code-agents/l7/)
