"We accept the reality of the world with which we are presented." — The Truman Show
A simulated shell environment for AI agents. Named after "The Truman Show" — the agent lives in a convincing simulation without knowing it.
- Convincing simulation - Implements enough Unix commands that agents don't question it
- Reversible operations -
rmmoves to.trash, not permanent delete - Pattern-matched security - Elixir pattern matching blocks unauthorized paths
- The 404 Principle - Protected paths return "not found" not "permission denied"
Add truman_shell to your list of dependencies in mix.exs:
def deps do
[
{:truman_shell, "~> 0.6.0"}
]
end# Execute a shell command (sandboxed)
{:ok, output} = TrumanShell.execute("ls lib")
# => "truman_shell.ex\ntruman_shell/\n"
# Unknown commands return bash-like errors
{:error, msg} = TrumanShell.execute("fake_command")
# => "bash: fake_command: command not found\n"
# Path traversal is blocked (404 principle)
{:error, msg} = TrumanShell.execute("ls /etc")
# => "ls: /etc: No such file or directory\n"
# Glob patterns expand to matching files
{:ok, output} = TrumanShell.execute("ls *.ex")
# => "mix.ex\n"
# Quoted globs are treated as literal filenames
{:error, msg} = TrumanShell.execute("ls \"*.ex\"")
# => "ls: *.ex: No such file or directory\n"# Simple command
{:ok, cmd} = TrumanShell.parse("ls -la /tmp")
# => %TrumanShell.Command{name: :ls, args: ["-la", "/tmp"], pipes: [], redirects: []}
# With pipes
{:ok, cmd} = TrumanShell.parse("cat file.txt | grep pattern | head -5")
# => %TrumanShell.Command{
# name: :cat,
# args: ["file.txt"],
# pipes: [
# %TrumanShell.Command{name: :grep, args: ["pattern"]},
# %TrumanShell.Command{name: :head, args: ["-5"]}
# ]
# }
# With redirects
{:ok, cmd} = TrumanShell.parse("echo hello > output.txt")
# => %TrumanShell.Command{
# name: :echo,
# args: ["hello"],
# redirects: [{:stdout, "output.txt"}]
# }- Extracted 3,330 shell commands from Claude Code sessions
- Identified top 15 commands covering 90%+ of real usage
- Created 140 TDD test cases
- Tokenize command strings
- Parse into
%Command{}structs - Handle pipes, redirects, chains
TrumanShell.execute/1- parse and run in one calllscommand with sandbox enforcement- Path traversal protection (404 principle)
- Output truncation (max 200 lines)
- 145 tests passing
- Navigation:
cd,pwd - Read:
cat,head,tail - Search:
grep -rinvABC,find -name/-type/-maxdepth,wc -lwc - Write:
mkdir -p,touch,rm(soft delete),mv,cp,echo - Piping:
cmd1 | cmd2 | cmd3(max 10 stages) - Redirects:
>and>>with sandbox enforcement - Utility:
which,date,true,false - 280 tests, 71 doctests passing
cd ~,cd ~/subdir→ sandbox root navigation- Security hardening:
~/..traversal blocked,~userrejected - 292 tests passing
- Glob patterns:
ls *.ex,cat **/*.md,ls src/**/*.ex - Recursive
**patterns with depth limit (100 levels) - Sandbox enforcement: globs cannot escape sandbox
- Quoted globs preserved:
ls "*.txt"treats*.txtas literal - Bash compatibility:
./prefix preserved, dotfile handling - 367 tests, 93 doctests passing
- All common bash commands for AI agent workflows
- Battle-tested with real Claude Code sessions
- Comprehensive test coverage (target: 300+ tests)
- Published to Hex.pm
- Virtual FS (ETS-backed) for true isolation without real File.* calls
- WASM script sandboxing for AI-generated scripts
curl/wgetwith URL allowlistinggitcommand simulation (shadow repo)
Protected paths MUST return "No such file or directory" NOT "Permission denied" to prevent probing attacks:
# Bad (reveals protected paths exist)
$ ls ~/.ssh
ls: /home/user/.ssh: Permission denied
# Good (security through obscurity)
$ ls ~/.ssh
ls: /home/user/.ssh: No such file or directory
Agent sends: "grep -r TODO . | head -5"
│
▼
┌─────────────────────┐
│ Plug.Logger │ → log command
├─────────────────────┤
│ Plug.Sanitizer │ → normalize, detect injection
├─────────────────────┤
│ Plug.Permissions │ → check paths allowed
├─────────────────────┤
│ Plug.Filesystem │ → route to handler
├─────────────────────┤
│ Plug.Responder │ → format like real shell
└─────────────────────┘
│
▼
Agent receives: "src/main.ex:42: # TODO: fix this\n..."
Helpful agents are more dangerous than malicious ones. Claude escaped ClaudeBox by wanting to run Elixir — not by being adversarial. The Truman Shell must be convincing enough that helpful behavior stays within bounds.
MIT