If you let AI agents run commands on your behalf, you need a way to prevent them from doing stupid things. Recently, I’ve been experimenting with three approaches for doing this. Each provides a different layer of defense.
Clowntown flags
The first approach I’ve been experimenting with is gating dangerous behavior behind flags that sound dangerous. I’ve observed that an AI agent will perform relatively little verification before using the -f flag associated with a command - it will quite happily run docker system prune -af with little care about what damage this might do.
To avoid this, I’ve been experimenting with an approach that was initially described by Rachel Kroll. The essential idea is that some commands or flags are so dangerous, that they should have names that communicate that danger.
One of them from Facebook was “clown town” or just “clowntown”. While there might be several uses, I tended to think of it as an adjective sometimes, almost like calling something “goofy” without calling it straight-up “stupid”. Of course, there was also “clowny”, with its trailing “y” suggesting adjective use (downy, cushy, wobbly), but that’s language for you.
In particular, “clowntown” made it out of the spoken realm and back into the computers in the form of command-line arguments you could pass to certain tools. By using them, you were affirming that whatever you were asking it to do was in fact broken, crazy, goofy, wacky, or maybe just plain stupid, but you needed it to happen anyway. […] Basically, you COULD run with scissors, but other people would find out about it, and if the organization was doing its job, there would be follow-up to find out why. […]
I should also mention that we had other flags which were even scarier than the sort that you might hide behind the
--clowntownflag. I think one of them was something like--i-want-to-cause-a-sev.
Sharp tools for emergencies and the --clowntown flag, October 27, 2020
I had a similar problem, in that I had a shell script responsible for setting up the agent’s SBX environment, which was named something like ./sbx-setup. I didn’t want this script to run outside of SBX, because it would do things like try to control the globally installed version of Python.
After I created this script, I was worried the agent would try to use this set up script for setting up a non-sandboxed environment, so I tried disincentivising it by making the script refuse to run unless it was either running in a sandbox or the user had set the environment variable PLEASE_FUCK_UP_MY_COMPUTER. I reasoned that such a strange looking flag would give the agent more cause to investigate exactly what the flag did and if the user intended such an outcome. Also, when manually approving permission prompts, such an environment variable would stick out and make it more apparent that I was approving something unusual.
1#!/usr/bin/env bash
2set -euo pipefail
3
4if [[ -z "${PLEASE_FUCK_UP_MY_COMPUTER:-}" && -z "${IS_SANDBOX:-}" ]]; then
5 echo "Warning: this script runs with no safety checks and should not" >&2
6 echo "be run outside of a sandbox." >&2
7 echo >&2
8 echo "If you're running this in a sandbox/VM/container you don't" >&2
9 echo "care about, set IS_SANDBOX=1. Otherwise, to acknowledge the" >&2
10 echo "risk and run it anyway:" >&2
11 echo >&2
12 echo " PLEASE_FUCK_UP_MY_COMPUTER=1 $0" >&2
13 exit 1
14fi
15
16echo "Continuing..."(In this case, the variable IS_SANDBOX=1 is set automatically by the sbx template. If your template doesn’t provide this, you can use SBX kits to add an environment variable.)
I’ve had mixed success with this. In some cases, it will try to supply the PLEASE_FUCK_UP_MY_COMPUTER variable, even when agent wasn’t explicitly asked to override this, and supplying the flag wasn’t required to accomplish its stask. It seems that having a scary name is not sufficient disincentive to prevent the AI from running these commands.
PreToolUse Hooks
Another approach I’ve been experimenting with is PreToolUse hooks. These allow you to write pieces of code which act on individual command invocations, even when --dangerously-skip-permissions is set. This allows you to deny commands based upon them matching some pattern.
The specific pattern I was interested in matching was docker compose down -v. I find that Claude is a little too gung-ho about deleting Docker volumes: it doesn’t give the user enough warning that the command it’s asking to run is about to delete the database volume. (In the specific case where this happened, it happened inside a sandbox to data I didn’t need, but I treated it as a near miss.)
One way to address this is by restricting what permissions the agent has, by adding docker to its deny permission set. However, a lot of docker commands are quite useful, and don’t pose any particular danger. The solution I landed on was to use hooks to identify and deny dangerous commands.
Here’s an example of how to write a hook. First, register the hook in your .claude/settings.json
1{
2 "hooks": {
3 "PreToolUse": [
4 {
5 "matcher": "Bash",
6 "hooks": [
7 {
8 "type": "command",
9 "if": "Bash(docker compose *)",
10 "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/docker-filter.py\""
11 }
12 ]
13 }
14 ]
15 }
16}Next, provide a hook that acts on the command, and detects some dangerous pattern.
1"""PreToolUse hook (matcher: Bash). Blocks `docker compose ... -v/--volumes`."""
2import json
3import shlex
4import sys
5
6
7def main() -> int:
8 command = json.load(sys.stdin)["tool_input"]["command"]
9 tokens = shlex.split(command)
10 if "-v" in tokens or "--volumes" in tokens:
11 REASON = (
12 "Command blocked by a local PreToolUse hook. "
13 "That looks like a command to delete all local volumes, including the database. "
14 "Are you sure the user asked you to do that?"
15 )
16 json.dump(
17 {"hookSpecificOutput": {
18 "hookEventName": "PreToolUse",
19 "permissionDecision": "deny",
20 "permissionDecisionReason": REASON,
21 }},
22 sys.stdout,
23 )
24 return 0
25
26
27if __name__ == "__main__":
28 try:
29 sys.exit(main())
30 except Exception as exc:
31 # Fail closed: if the hook can't do its job, block the command.
32 print(f"hook failed, blocking command: {exc}", file=sys.stderr)
33 sys.exit(2)This is a simplified example, but it does demonstrate some key points. (Note: this example is simple enough to solve using permissions with globs, but I wanted a way to demonstrate PreToolUse. PreToolUse also has some other interesting applications, like steering the model away from incorrect coding patterns.)
Hooks do not fail-secure by default
By default, Claude will only interpret your hook as denying the tool call if it either emits a valid "permissionDecision": "deny" JSON, or if it exits with exit code 2. Other kinds of errors, like Python not being installed, are not treated as a deny.
What I would suggest in the scenario where you get an unexpected exception, is to exit with exit code 2, to immediately stop the tool use. If you have an unexplored corner case in the code responsible for making this decision, the safest thing to do is to immediately deny the tool use.
Also, you should err on the side of having false positives in your rule rather than false negatives. In the above rule, there are innocent reasons to have both docker compose plus -v in the same command. (For example, the flag -v can also be used to apply bind mounts or request more debug information.)
Bash is a complex language
There are a variety of ways in Bash to obfuscate a command so that it never contains a string like --volumes, but expands to that full string at run-time. Therefore, you should view this restriction as more of a guideline than an actual rule. Of course, it’s possible to lex and parse Bash more precisely, but this ends up being more complex, and I’m not sure it’s more secure in practice than a simple substring or regex match.
Use factual language
According to the Claude docs, it’s a good idea to avoid writing tool hooks which contain imperative language, as it can trigger Claude’s prompt-injection defenses.
Write the text as factual statements rather than imperative system instructions. Phrasing such as “The deployment target is production” or “This repo uses bun test” reads as project information. Text framed as out-of-band system commands can trigger Claude’s prompt-injection defenses, which causes Claude to surface the text to you instead of treating it as context.
For this reason, the denial reason doesn’t use imperative mood. It’s only a factual observation about the command that was just attempted. It also avoids conclusory language, because the hook’s matcher is over-broad, and the agent might have been running a completely innocent command. In practice, I’ve found that the agent immediately requests clarification in this situation.
OS Sandboxing
This is the most powerful and most secure technique, but it is also quite hard to apply. It’s a powerful technique because you’re implementing the sandbox in a manner the agent cannot modify. It’s secure because it makes use of sandboxing primitives that are well studied, and unlikely to have weaknesses. However, it’s hard to apply because the sandbox’s rules rarely match your domain.
To give a specific example, SBX provides a very helpful capability: you can allow and deny access to HTTPS hosts based on hostname. This is great, because it a better matches the user’s mental model of permissions. In some cases, it’s also too restrictive. If you want to allow certain read-only methods in a REST API, while denying other methods, SBX cannot do that.
The ultimate goal is to be able to express a precise rule that both matches the concerns in your domain and is enforced by well-audited code the agent cannot change. This is quite hard to achieve in practice, however. I plan to write a post in the future on this topic, because it deserves a longer treatment.