Chapter 5 of 15

Giving the Model Tools Without Giving It the Keys

Tool access is a policy surface, not a convenience feature. Once the model could touch files and run commands, I treated it much more like an untrusted developer operating through a constrained API. That framing made the security decisions much clearer than relying on instructions in a prompt.

  • File operations
  • Command execution
  • Path containment
  • Timeouts, output limits, and policies
  • Secret handling

The situation

Once the agent could run commands and edit files, it effectively had the same capability as a developer with a terminal. In Giving the Model Tools Without Giving It the Keys, I wanted to capture the real operating situation rather than describe the problem in abstract terms. What mattered to me was the moment the existing approach stopped being sufficient in normal use, because that was usually the point where the next architectural requirement became obvious.

The naive solution

Give the model a generic "run shell command" tool and trust the prompt to keep it well-behaved. The naive option was attractive precisely because it was faster and easier to explain. I could have pushed further in that direction and probably improved the demo, but it would have optimised the visible symptom rather than the underlying system constraint.

The hidden constraint

Prompts are not a security boundary; an unconstrained shell tool can escape the project directory, run indefinitely, or exfiltrate secrets into logs and model context. This constraint was easy to miss early because it did not show up in the first successful run. It appeared once the workflow had to survive repetition, failure, another user, another provider, or a return visit, which is why I now look for these second-order requirements much earlier.

The decision

Treat every tool as a policy-bound capability: the platform decides which tools exist, which paths are valid, how long commands may run, how much output returns, and which secrets are injected. I treated this as an architectural or product decision rather than a prompt tweak. The aim was to create a boundary I could reason about later, instead of relying on model behaviour to remain consistent as providers, templates, and user requests changed.

The implementation

Added workspace containment and path-traversal protection, allowed-command policies, shell/inline-code restrictions, timeouts, output limits, and runtime secret injection that never places raw values in generated files. I also tried to make the implementation concrete enough that the decision could be inspected in code and runtime behaviour. That meant explicit state, explicit policy, and observable transitions rather than a collection of hidden assumptions spread across prompts and handlers.

The tradeoff

Some legitimate developer workflows are harder or impossible for the agent until an explicit policy is added for them. I did not treat the decision as free. In most cases I was deliberately accepting more engineering, state management, or operational complexity in exchange for a smaller failure surface and a product that could survive realistic use.

The evidence

Command execution records capture exit codes and bounded output excerpts rather than raw unbounded terminal streams, and secrets are redacted before reaching logs or tool responses. I use this evidence as a practical signal, not as a claim that the architecture is universally correct. The useful question was whether the new design changed what the system could reliably do under the conditions that had previously broken it.

The reusable lesson

Design tool access like an API surface for an untrusted caller, because that is what a model with tool access effectively is. The part I would carry into another build is the decision rule behind the implementation, not the exact technology choice. Vendors and frameworks will change; the underlying question about state, control, isolation, feedback, or user trust is much more reusable.

The consultant's questions

  • If this tool call went wrong or was manipulated, what is the blast radius?
  • Where do your secrets live relative to the model's context window?
  • What assumption in this part of the system would become dangerous if usage or tenant count increased?
  • What evidence would tell me this decision needs to be revisited rather than defended indefinitely?