PromptABCD
FeaturesLearnHow it worksUse casesFAQGuideBlogContext Blocks
Sign inGet started free
Sign inSign up
PromptABCD

A calm home for your best AI prompts. Save them once, find them in seconds, reuse them forever.

Product

  • Features
  • Chrome Extension
  • Free Courses
  • How it works
  • Use cases
  • Blog
  • Context Blocks
  • Export Anywhere
  • FAQ

Resources

  • User guide
  • Learn prompting
  • Sign in
  • Get started free

© 2026 PromptABCD. All rights reserved.

Privacy PolicyTerms and Conditions
Home/Blog/AI Harness/Building a Safe Shell Tool for Your Agent Harness
AI Harness

Building a Safe Shell Tool for Your Agent Harness

Agent shell tool safety comes down to allowlists, argument validation, and never using shell=True. A step-by-step guide to a shell tool you can trust.

August 28, 2026·9 min read
ShareShare
⚡Featured Prompt— copy and use right now
import subprocess

def shell_tool(argv: list[str], timeout=15):
    if not isinstance(argv, list) or not argv:
        return "ERROR: expected a non-empty list of arguments"
    try:
        out = subprocess.run(argv, capture_output=True, text=True,
                             timeout=timeout, shell=False)  # never True
        return (out.stdout + out.stderr)[:6000] or "(no output)"
    except subprocess.TimeoutExpired:
        return f"ERROR: '{argv[0]}' timed out after {timeout}s"
    except FileNotFoundError:
        return f"ERROR: command not found: {argv[0]}"

Picture this: you're a platform engineer, it's Friday afternoon, and you just gave your agent a

shell
tool so it can run
git status
and
ls
for you. It's working beautifully. Then, testing an edge case, you watch it run
rm -rf
on a path built from a variable it misread. Your stomach drops. The command was valid. The tool ran it. Nothing was malicious — the harness simply did exactly what it was told. Agent shell tool safety is the discipline that stands between "handy automation" and that Friday-afternoon feeling.

This guide builds a shell tool you can actually trust, step by step.

Quick-Start (Copy This Right Now)

The first rule is the one people skip: never build a shell string and never use

shell=True
. Pass arguments as a list so nothing the model writes can inject extra commands.

hljs python
[object Object], subprocess

,[object Object], ,[object Object],(,[object Object],):
    ,[object Object], ,[object Object], ,[object Object],(argv, ,[object Object],) ,[object Object], ,[object Object], argv:
        ,[object Object], ,[object Object],
    ,[object Object],:
        out = subprocess.run(argv, capture_output=,[object Object],, text=,[object Object],,
                             timeout=timeout, shell=,[object Object],)  ,[object Object],
        ,[object Object], (out.stdout + out.stderr)[:,[object Object],] ,[object Object], ,[object Object],
    ,[object Object], subprocess.TimeoutExpired:
        ,[object Object], ,[object Object],
    ,[object Object], FileNotFoundError:
        ,[object Object], ,[object Object],

What this does: it runs a command from an argument list, with the shell disabled, so

["ls", "; rm -rf /"]
runs a program literally named
; rm -rf /
(which doesn't exist) instead of chaining a destructive command. Disabling the shell closes the entire injection class in one line. This is the foundation everything else sits on.

Understanding the Variables

Three ideas drive shell tool safety, and getting them straight up front saves you from the two dead-end approaches most people try first.

Allowlist, not blocklist. The tempting move is to ban dangerous commands — block

rm
,
curl
,
dd
. Blocklists always lose, because there are infinite ways to spell danger (
rm
,
/bin/rm
,
find -delete
, a Python one-liner). An allowlist inverts the problem: only named commands run, and everything else is refused by default. You reason about a short list of permitted actions instead of an infinite list of forbidden ones.

Argument validation, not just command validation. Allowing

git
isn't enough —
git
can push, reset, and delete branches. Safety lives at the argument level: allow
git status
and
git log
, refuse
git push
unless you meant to.

No shell interpretation, ever. Covered above, but it's the load-bearing idea. The moment you interpolate model output into a string a shell parses, you've lost.

Worth understanding why the argument-list form is safe, because the reason is precise. When you call

subprocess.run(["ls", user_input])
, the operating system receives
ls
and its arguments as separate, already-split values — there's no shell in between to interpret semicolons, pipes, backticks, or
$()
substitutions. Those metacharacters only have power when a shell parses a single combined string. Remove the shell and they become ordinary text, passed literally to the program as an argument it'll almost certainly reject. That's the whole mechanism, and it's why "just sanitize the input" is the wrong fix: you don't need to sanitize what's never interpreted. Blocklisting characters is an arms race; removing the interpreter ends the war.

⚡ Pro tip: Log the exact

argv
list for every call before it runs, not after. When something goes wrong, the pre-execution log shows you precisely what the harness was about to do — including the malformed argument that caused the problem — which the post-execution log can miss if the command hangs.

Agent Shell Tool Safety, Step by Step

Now build the allowlisted version. Each permitted command gets a validator that inspects its arguments:

hljs python
ALLOW = {
    ,[object Object],:    ,[object Object], a: ,[object Object],(,[object Object], x.startswith(,[object Object],) ,[object Object], x ,[object Object], (,[object Object],, ,[object Object],) ,[object Object], x ,[object Object], a),
    ,[object Object],:   ,[object Object], a: a ,[object Object], a[,[object Object],] ,[object Object], (,[object Object],, ,[object Object],, ,[object Object],, ,[object Object],),
    ,[object Object],:   ,[object Object], a: ,[object Object],(a) == ,[object Object], ,[object Object], ,[object Object], ,[object Object], ,[object Object], a[,[object Object],],
    ,[object Object],:  ,[object Object], a: ,[object Object],,
}

,[object Object], ,[object Object],(,[object Object],):
    ,[object Object], ,[object Object], argv:
        ,[object Object], ,[object Object],
    cmd, args = argv[,[object Object],], argv[,[object Object],:]
    ,[object Object], cmd ,[object Object], ,[object Object], ALLOW:
        ,[object Object], ,[object Object],
    ,[object Object], ,[object Object], ALLOW[cmd](args):
        ,[object Object], ,[object Object],
    ,[object Object], shell_tool(argv, timeout)   ,[object Object],

What this does: it checks the command against an allowlist, runs that command's argument validator, and only then hands off to the shell-disabled runner. A blocked command returns a clear message listing what is allowed, so the model corrects itself instead of flailing. Notice

git
permits read operations and silently excludes
push
and
reset
— the model can't destroy history because destruction was never on the menu.

⚡ Pro tip: Make the "allowed commands" list part of the tool description the model reads. When the model knows up front that only

status
,
log
,
diff
, and
branch
are available under
git
, it stops asking for
git push
and your rejection rate drops. Describe the boundary and the model respects it far more often than it tests it.

Pro-Level Variations

Three upgrades cover most real deployments:

Run inside a jailed working directory. Set

cwd
to a dedicated scratch folder and validate that no argument resolves outside it. This pairs allowlisting with containment, so even a permitted
cat
can't wander into
/etc
.

Add a confirmation gate for a second tier. Some teams keep a small set of "allowed with human approval" commands — the agent proposes, a person clicks yes. The harness pauses, surfaces the exact

argv
, and only runs on approval.

Drop the whole thing in a container. For untrusted contexts, combine the allowlist with the disposable-container pattern so even a validator bug can't escape the box.

Three teams putting this to work:

  • A DevOps engineer gives an incident-response agent read-only shell access —
    kubectl get
    ,
    logs
    ,
    describe
    — with write commands excluded entirely, so the agent can diagnose an outage but never change cluster state.
  • A data engineer allows
    dbt run
    and
    dbt test
    with validated model names, so the agent can trigger pipelines but can't run arbitrary SQL against production.
  • A QA automation lead permits a fixed set of test-runner commands in a container, letting the agent reproduce failing tests without any path to the host.

Auditing What the Agent Actually Ran

Safety isn't only about prevention — it's about being able to answer "what did the agent do?" after the fact. A shell tool should keep a durable record of every command it executed, separate from the model's transcript, because the transcript can be summarized or trimmed while an audit log must be complete.

hljs python
[object Object], json, time

,[object Object], ,[object Object],(,[object Object],):
    entry = {,[object Object],: time.time(), ,[object Object],: argv}
    result = safe_shell(argv, timeout)   ,[object Object],
    entry[,[object Object],] = result.startswith(,[object Object],)
    entry[,[object Object],] = result[:,[object Object],]
    ,[object Object], ,[object Object],(logfile, ,[object Object],) ,[object Object], f:
        f.write(json.dumps(entry) + ,[object Object],)
    ,[object Object], result

What this does: it appends a structured record of every attempted command — the exact arguments, whether it was blocked, and a preview of the output — to an append-only log before returning. When someone asks what the agent touched last Tuesday, you have the answer, and when the agent gets blocked repeatedly, the log shows you which command it keeps reaching for so you can decide whether to allow it.

This log is also your best signal for tightening the allowlist safely. If a command has been allowed for a month and never once used, remove it — an unused permission is pure risk with no benefit. If a blocked command appears constantly and it's genuinely safe, that's your cue to add it deliberately. The audit log turns allowlist maintenance from guesswork into evidence.

⚡ Pro tip: Alert on the rate of blocked commands, not just their presence. A sudden spike in blocked attempts often means the model has gone off the rails — misunderstanding the task and probing for tools it shouldn't need. That spike is an early warning that something's wrong with the run, visible in the audit log before it shows up in the output.

A compliance-focused engineer at a bank uses exactly this log to satisfy an auditor's requirement that every automated action against infrastructure be reconstructable months later — the append-only JSONL is the evidence trail, independent of whatever the model's context window remembers.

Troubleshooting Common Issues

⚠️ Common mistake: Reaching for

shell=True
because a command "needs" a pipe or a wildcard. It doesn't — do the piping in Python. Run each stage as a separate
subprocess
call and connect them in code, or expand globs with the
glob
module before building your argv. Every time you enable the shell to save a few lines, you re-open the injection door you spent this whole guide closing.

Other issues you'll hit:

  • The agent keeps requesting blocked commands. Your tool description doesn't list what's allowed. Add the allowlist to the description and the requests realign.
  • A permitted command hangs. Something's waiting on stdin. Pass
    stdin=subprocess.DEVNULL
    so an interactive prompt fails fast instead of freezing the run.
  • Validation is too strict and blocks legitimate work. Loosen the specific validator, not the allowlist as a whole. Widening one command's arguments is safe; adding a command needs real thought.
  • The agent quotes a whole command as one argument. You'll see it pass
    ["git status"]
    instead of
    ["git", "status"]
    . Split on whitespace in the tool description's examples so the model learns to send pre-tokenized arguments, and validate that the first element is a bare command name with no spaces.

Your Turn

Start with the two non-negotiables today: argument lists instead of strings, and

shell=False
. Those alone close the injection class that causes the worst incidents. Add the allowlist the first time you feel nervous about what the agent might run, and reach for containers when the code's source stops being trustworthy. Build up the layers as the threat grows — don't front-load paranoia you don't need yet.

The allowlists and command descriptions that make a shell tool safe are worth keeping as reusable assets, because every new agent that touches a shell needs the same guardrails. Storing your proven allowlist-plus-description pairs in a prompt library like PromptABCD — tagged by the environment they're safe in — means your next shell agent starts locked down by default. Agent shell tool safety is a habit, and habits are easier to keep when the safe version is the one you can paste in from a library you already trust.

agent shell tool safetyshellsubprocessai agentssecuritytutorial

Continue Reading

The SWE-bench Harness Explained for Agent Builders
AI Harness

The SWE-bench Harness Explained for Agent Builders

The swe-bench harness fails logically correct patches when the environment is wrong. Learn how it grades, what FAIL_TO_PASS means, and how to run it yourself.

August 28, 2026·8 min read
Building an Evaluation Harness for Your Agent
AI Harness

Building an Evaluation Harness for Your Agent

The best way to build agent eval harness infrastructure isn't LLM-as-judge. Learn to design verifiable tasks and programmatic graders you can actually trust.

August 28, 2026·8 min read
What Is an Eval Harness, and Why Do Agents Need One?
AI Harness

What Is an Eval Harness, and Why Do Agents Need One?

An AI eval harness tells you your agent works across a hundred tasks, not just the one you tried. Learn what it measures and why agents need it more than models.

August 28, 2026·8 min read

Save the prompts from this post

PromptABCD is a free prompt manager. Paste, organize, and reuse your best AI prompts — no more hunting through chat history.

Start free →
← PreviousHow to Sandbox Code Execution in Your Agent HarnessNext →File System Access in an Agent Harness: A Case Study
Share this post:
ShareShare