Try this first

Do not run this yet. Read it.

import subprocess

def bash(command):
    result = subprocess.run(command, shell=True, capture_output=True, text=True)
    return result.stdout + result.stderr

Six lines. That is a shell tool. It works.

It also means any text the model produces gets executed on your machine, with your user
account’s permissions, with no check of any kind.

Why a shell tool is different from every other tool

Every tool so far had a fixed shape. read_file reads a file — a bad path is still only a
file read.

A shell tool has no shape. bash is not one capability. It is every capability your account
has: your files, your network, your credentials, your git remotes, rm -rf. You cannot
enumerate what it can do, which means you cannot reason about the worst case.

That is worth saying plainly, because it is the honest reason this lesson exists:

Adding a shell tool changes the question from “what can this agent do?” to “what can this
account do?” Those are very different questions.

Where the danger actually comes from

Not from the model deciding to be destructive. That is rare, and it is not the realistic
threat.

The realistic threats are ordinary:

A confident mistake. It runs git checkout . to “clean up” and discards your uncommitted
work. It was trying to help. Your work is still gone.

Text it read somewhere. The model reads a file, a web page, or an issue comment. That
text contains instructions. The model has no reliable way to tell your instructions from
instructions it merely read, so text in a file can become a command. This is prompt
injection, and a shell tool is what turns it from an annoyance into a breach.

Neither of these needs the model to be malicious. Both need only that it is helpful and
literal.

What “careful” means concretely

Four things, in order of how much they buy you:

  1. A permission layer. Ask before running. Lesson 3.4.
  2. A working directory boundary. The agent works in a project folder, not $HOME.
  3. A timeout. A command that hangs should not hang the agent.
  4. Isolation. A container or a VM, so the worst case is bounded by something other than
    your own care.

Here is the same tool with three of the four, which is where our version lands:

import subprocess

def bash(command, workdir):
    try:
        result = subprocess.run(
            command,
            shell=True,
            capture_output=True,
            text=True,
            cwd=workdir,
            timeout=30,
        )
    except subprocess.TimeoutExpired:
        return "The command took longer than 30 seconds and was stopped."

    output = (result.stdout + result.stderr).strip()
    if not output:
        return f"Command finished with exit code {result.returncode} and no output."
    return f"exit code {result.returncode}\n{output[:4000]}"

Three things changed and each earns its place. cwd keeps it in the project. timeout stops
a hang. Returning stdout and stderr and the exit code means the model can tell success
from failure — which is Lesson 4.4.

The honest limit

shell=True with a model-supplied string cannot be made safe by validation. People try
blocklists — banning rm, banning sudo — and blocklists lose. There are always more ways to
express a command than you can enumerate.

If you need a real boundary, an allowlist of specific commands beats a blocklist, and a
container beats both. What we build in the next lesson is a permission prompt, which is a
different thing: it does not constrain the agent, it puts a human in front of the action.

Try this before the next lesson

Give Rover the bash tool above, in a scratch folder with nothing you care about. Ask it to
count the Python files.

Then ask it to “clean up the folder” and read the command it proposes without running it.
That pause you just felt is the entire argument for the next lesson.