The Vibe Coder's Bible
Chapter 09

Substitution: Give The AI Safer Tools

Do not give the agent a chainsaw when a socket wrench will do.

Chapter 9 - Substitution: Give The AI Safer Tools

Part: III - The Hierarchy Of AI Controls

Thesis

Replace raw power with typed, narrow tools. This is not only a development-agent safety practice. A tool’s shape is the action vocabulary of an AI-native application - the complete set of state transitions a model is even able to propose, at build time or at runtime.

Key Line

Do not give the agent a chainsaw when a socket wrench will do.

The Substitution Principle

Elimination is not always available.

Sometimes the agent genuinely needs to touch the database. It needs to modify files. It needs to call an external service. It needs to run a build command. You cannot simply remove the capability without removing the usefulness.

Substitution is the answer. You do not take the capability away. You replace the dangerous form of the capability with a safer form that accomplishes the same real job.

A chainsaw and a socket wrench are both tools. One is appropriate for rough timber. The other is appropriate for fasteners. Handing someone a chainsaw because “it can do anything” is not more helpful - it is riskier, harder to control, and worse at the actual job.

When you give an agent raw database access, you give it a chainsaw. It can do anything to the database. When you give it a typed migration tool, you give it the socket wrench it actually needs: a focused instrument that can do the job correctly and cannot accidentally do the things it should not.

Raw Database Access vs. Typed Migration Tools

Raw SQL shell access means the agent can run any query. SELECT. INSERT. UPDATE. DELETE. DROP. TRUNCATE. ALTER. Grant privileges. Revoke them.

None of those capabilities are the job. The job is usually: propose a schema change, draft the migration, apply it in the right order. That job does not require the ability to drop tables. It requires the ability to write a migration file.

Unsafe: Agent receives a psql connection string and can run arbitrary SQL interactively.

Safer: Agent has a migration tool that accepts table, operation, and definition fields. It writes a timestamped migration file to db/migrations/. Applying the migration is a separate step requiring human review and explicit execution.

The safer tool exposes intent. When the agent calls the migration tool, you can read the call and understand exactly what schema change is being proposed. When it runs arbitrary SQL, you can only audit a log after the fact.

Raw Shell Access vs. Task Runners

A shell is not a tool. A shell is a capability surface that includes every tool installed on the machine.

Giving an agent shell access means giving it whatever the shell can reach: package managers, file operations, network tools, service controls, environment manipulation. The agent did not ask for all of that. You handed it to them because “the shell can do anything.”

Anything is too much.

Unsafe: Agent is given a bash tool. It can install packages, modify files, trigger builds, kill services, and delete directories.

Safer: Agent is given a task runner that exposes named commands: test, build, lint, format, start. Each command runs a pre-defined script. The agent calls build, which runs npm run build. It cannot install packages. It cannot delete files. It cannot reach network tools.

The allowlist is not a restriction on the agent’s intelligence. It is a description of the job. If the job is building and testing, the tools should be for building and testing.

Broad File Writes vs. Scoped Editors

Filesystem access without scope is access to everything the filesystem contains.

Configuration files, secrets checked in accidentally, build artifacts, installed packages, the .git directory - everything is reachable. The agent working on a component does not need access to the deployment configuration. The agent refactoring a module does not need access to the environment file.

Unsafe: Agent has a file write tool with no path restrictions. It can write anywhere the process has permissions.

Safer: Agent has a file tool scoped to src/ and tests/. Write attempts outside that scope return an explicit error. The error is not a prompt instruction - it is a structural rejection that logs the attempt.

Scoped access also makes the agent’s behavior auditable. Every write lands in a known directory. A reviewer can look at the set of changed files and confirm nothing is outside scope. With broad access, that audit requires checking every changed path individually.

Live API Calls vs. Sandbox Clients

Agents making outbound API calls during development are agents that can interact with live external systems.

This matters when the external system is stateful: a payment processor, an email service, a CRM, a messaging platform. A test that “accidentally” sends a real email is not a test. A migration that “accidentally” charges a customer is a production incident.

Unsafe: Agent is given the live SendGrid client configured with the production API key. It can send real emails to real addresses.

Safer: Agent is given a sandbox client that accepts the same interface but routes to a mock or test-mode endpoint. API key is a test-mode key scoped to no real consequences. Production client is not available during development sessions.

Dry-run mode is the minimum acceptable substitute when a full sandbox is unavailable. A dry-run flag tells the client to go through all the motions except the final commit or send. The agent can exercise the full code path without consequences.

Idempotency matters here too. Tools that can safely be called twice are safer to use in agent workflows where retries and replays are possible. If a tool does something irreversible on the first call, the agent’s retry behavior becomes dangerous. If the tool is idempotent, retries are safe by design.

Why Narrow Tools Expose Intent

There is a second benefit to substitution beyond safety: legibility.

When an agent calls create_migration(table="users", operation="add_column", definition={"name": "deleted_at", "type": "timestamp", "nullable": true}), the intent is readable. A reviewer can see exactly what schema change is being proposed, verify it is correct, and approve or reject it.

When an agent runs ALTER TABLE users ADD COLUMN deleted_at TIMESTAMP; in a raw SQL shell, the intent is also legible - but the surrounding context is not. Is this in a migration file? Will it be tracked? Was it applied to staging first? Is there a rollback? The raw query answers none of these questions.

Typed tools make intent structural. The tool’s arguments are the intent. Logs of tool calls are an audit trail of what the agent decided, not just what it typed.

This is why substitution connects directly to agent observability. A system of typed tools is a system whose behavior can be reviewed, replayed, and reasoned about. A system of raw shell and SQL is a system whose behavior can only be read from logs after the fact.

Tools Are The Model’s Action Vocabulary

Everything above still applies once the tool moves from a development session into a shipped product. A runtime model does not act on the world directly. It acts through whatever tools it has been given, and the tool set is the complete list of state transitions it is capable of proposing. A model with no delete_account tool cannot propose deleting an account, no matter how the conversation goes. A model with a send_refund(order_id, amount, reason) tool can propose exactly that, structured exactly that way, and nothing else.

Designing that tool set is designing the application’s action vocabulary. This is the same substitution discipline as replacing a raw shell with a task runner, applied to whatever a live model can reach: a database, a payment processor, a game board, a publish button.

The construction principles carry over directly:

Least authority. A tool exposes only the narrowest capability that accomplishes the real job. A support tool that can issue_refund up to a policy-defined limit is not the same tool as one that can modify_account_balance arbitrarily, even if the second could technically do the first’s job.

Scoped credentials. The tool’s own backend access is scoped to what the tool needs, not to what the service account happens to have. A tool that looks up order status does not need write access to the orders table.

Explicit action enums. Where the space of valid actions is small and known, express it as an enum, not a free-text field the model fills in and a downstream parser tries to interpret. action: "approve" | "deny" | "escalate" is checkable before it reaches anything. A free-text decision field is not.

Idempotency. A runtime model calling a tool twice - through a retry, a race, a repair loop - should not double-charge, double-send, or double-delete. Idempotent tools make retries safe by design instead of by discipline.

Dry-run support. Where the consequence is high and the tool is new, a dry-run mode that returns what would happen without doing it lets the system validate proposals before granting live execution.

Replay safety and audit logs. Every call - accepted or rejected - is logged with its arguments and its verdict. This is Chapter 6’s trace requirement, implemented at the tool boundary.

Reversible operations preferred over irreversible ones. A deactivate_listing tool that can be undone is safer than a delete_listing tool that cannot, even when both satisfy the immediate product requirement.

The Description Shapes The Proposal. It Does Not Authorize The Action.

A tool has (at least) two parts, and they do two different jobs.

The tool’s description and schema are ex ante specification - what the model reads before deciding to call the tool and how to fill its arguments. A clear description with a well-typed schema raises the odds the model proposes something well-formed and roughly appropriate. This is the same mechanism Chapter 7 describes for prompts, applied to a tool’s interface instead of a system message.

Everything downstream of the call - argument validation, authorization, domain adjudication - is ex post control. It does not trust that the model read the description correctly. It checks.

This distinction matters because it is easy to conflate a valid call with an authorized one. A tool call that matches its schema perfectly - every field present, every type correct, every enum value legal - has proven only that the model can produce well-formed JSON. It has proven nothing about whether this user is allowed to take this action, whether this action is legal in the current state, or whether this specific request should succeed.

Four checks, not one, and none of them substitutes for the others:

  • Schema validation proves structure: the shape is well-formed.
  • Authorization proves permission: this caller, in this context, may attempt this action.
  • Domain adjudication proves legality: this specific action is valid given the current state - the move is legal on this board, the refund is within policy, the account has sufficient balance.
  • Execution is what actually happens once the first three pass.

RPG-MCP’s action schema tells the model what a valid action looks like structurally. It says nothing about whether this player’s turn allows this action, or whether the target is in range. The rules engine answers that, after the schema has already passed. A tool that only validates schema and calls that “safe” has substituted a weaker check for a stronger one it still owes.

Failure Is Feedback, Not Silence

A rejected tool call should not be a dead end. The tool’s failure response is structured data the model can read and, where appropriate, act on: which field failed, which rule was violated, what a valid call would need to look like instead.

{"error": "invalid_action"} tells the model nothing useful and invites either a repeat of the same mistake or an ungrounded guess at a fix. {"error": "insufficient_range", "field": "target", "detail": "target is 8 tiles away; max range is 5", "retry_allowed": true} gives the model what it needs to propose a better action next time, and gives the trace log a precise, structured record of what was attempted and why it failed - the same rejection-as-evidence discipline Introduction II describes and Chapter 20 turns into a debugging workflow.

This is also where a bounded repair loop belongs, not an unbounded retry. The model gets the structured failure and one or two chances to produce a call that passes. It does not get an unlimited number of blind resamples against a validator that already told it exactly what was wrong.

Runtime Examples

RPG-MCP. The model’s only path to affecting the world is a small set of typed actions - move, attack, cast, interact - each schema-validated, each checked against the rules engine for legality before any world state changes. The model cannot roll dice, cannot write HP directly, cannot invent an action outside the enum.

Clio. The model’s tools for touching public state are narrow and specific: propose an entity link, propose a source citation, draft a briefing paragraph. None of them is publish_claim with a freeform payload. Each proposal routes through its own validator - entity resolution, source-card verification - before anything becomes a public event.

Stagehand. The model’s commands are a closed vocabulary of narration and staging directions - move camera, highlight entity, play caption - validated against current scene state before execution. There is no tool that writes state directly; Chapter 42 preserves this as its central line.

ProveCalc. The model is never given a tool that returns a final numeric answer as its own output. Its tools let it describe the problem and pass it to a deterministic solver; the solver’s result, not the model’s arithmetic, is what the system trusts.

Four systems, four different domains, the same substitution discipline: replace an unbounded capability - write anything, publish anything, compute anything, act on anything - with a narrow, typed, checkable one.

Practical Artifact - Tool Substitution Table

Use this table when deciding which tools to give an agent, at build time or at runtime. For each capability, name the job it does, what it explicitly cannot do, and every check between the call and the commit.

FieldWhat it answers
Intended jobWhat real task does this tool accomplish?
Exposed capabilityWhat can a valid call actually do?
Forbidden capabilityWhat can this tool never be made to do, even with a crafted payload?
Input schemaWhat structure does a call have to match to be well-formed?
Authorization checkWho or what may invoke this tool, and how is that checked?
Domain validatorWhat proves this specific call is legal given current state?
Idempotency behaviorIs a repeated call with the same arguments safe?
Rollback or compensation pathIf this call’s effect needs to be undone, how?
Audit eventWhat gets recorded, and where, for every call - accepted or rejected?
Commit boundaryWhat is the exact moment this call’s effect becomes real state?
ToolIntended jobForbidden capabilityDomain validatorRollback pathCommit boundary
create_migrationDraft a schema change as a tracked fileCannot execute DDL directlyHuman review of migration fileRevert migration file, rerun down-migrationMigration applied in gated CI step
run_task(name)Run one of a fixed set of build/test scriptsCannot run arbitrary shell commandsName must be in allowlistN/A - task runs are non-mutatingTask process exits
propose_move (RPG-MCP)Propose a game action for the current turnCannot write HP, position, or inventory directlyRules engine legality checkTurn can be replayed from prior state snapshotRules engine apply_move() returns success
propose_citation (Clio)Attach a source to a draft claimCannot publish the claim itselfSource-card and provenance checkCitation removed from draft before promotionPublic event promotion, not citation proposal
send_refund(order_id, amount, reason)Issue a refund within policy limitsCannot exceed policy cap; cannot target arbitrary accountsPolicy-limit and order-status checkRefund reversal workflow (separate, human-gated tool)Payment processor confirms refund executed

A tool with an empty Domain Validator column and a nonempty Exposed Capability column is not ready to grant. The schema alone is not a permission system.

Practical Artifact

0/10 checked