Why wsh?
AI agents run commands as your Unix user. They have the same permissions as you. Today, the options for restricting them are either nothing — or a week of sysadmin work that still leaves gaps. wsh fills that gap.
This page makes the case in three parts:
1. Side by side: standard tools vs wsh
Let’s say you want to let a coding agent run git, cargo, and rustc in a project directory, deny everything else, and log what it does. Here’s what that looks like with standard Linux tools:
Without wsh (~30 minutes, Linux only)
# 1. Create a restricted user for the agent
sudo useradd -m -s /bin/rbash agent-codex
sudo mkdir -p /home/agent-codex/bin
# 2. Symlink only the allowed binaries
sudo ln -s /usr/bin/git /home/agent-codex/bin/git
sudo ln -s /usr/bin/cargo /home/agent-codex/bin/cargo
sudo ln -s /usr/bin/rustc /home/agent-codex/bin/rustc
sudo ln -s /usr/bin/cat /home/agent-codex/bin/cat
sudo ln -s /usr/bin/ls /home/agent-codex/bin/ls
sudo ln -s /usr/bin/mkdir /home/agent-codex/bin/mkdir
# ... repeat for every coreutils command the agent needs
# 3. Lock down PATH so only symlinked binaries are available
echo 'export PATH="$HOME/bin"' | sudo tee /home/agent-codex/.bashrc
sudo chattr +i /home/agent-codex/.bashrc
# 4. Write an AppArmor profile
sudo tee /etc/apparmor.d/agent-codex << 'EOF'
#include <tunables/global>
profile agent-codex /home/agent-codex/** {
#include <abstractions/base>
/usr/bin/git rix,
/usr/bin/cargo rix,
/usr/bin/rustc rix,
/home/agent-codex/** rw,
deny /etc/shadow r,
deny /home/pete/** rw,
# ... 50+ more deny rules for every sensitive path
}
EOF
sudo apparmor_parser -r /etc/apparmor.d/agent-codex
# 5. Set up audit logging
sudo auditctl -a always,exit -F uid=$(id -u agent-codex) -S execve
sudo mkdir -p /var/log/agent-audit
# Now write a custom log parser...
# And a log rotation config...
# And figure out how to make the output readable...
# 6. Run the agent as the restricted user
sudo -u agent-codex -i codex --workdir /home/agent-codex/project
# 7. Review what happened (good luck)
sudo ausearch -ua $(id -u agent-codex) | aureport -x That’s a restricted shell, manual symlinks, a MAC profile, kernel audit rules, and a custom log pipeline. Every time you add a new tool, you touch four different configs. And the audit output is essentially raw syscall data.
With wsh (~2 minutes, Linux and macOS)
# 1. Install
curl -fsSL https://warrant.sh/install.sh | sudo sh
# 2. Set up policy for your agent
wsh setup codex
# 3. Review what happened
wsh audit Two commands. Policy enforcement, tamper-proof audit logging, shell integration, and human-readable output. Works on both Linux and macOS.
Maintenance comparison
| Task | Standard tools | wsh |
|---|---|---|
| Allow a new tool | Create symlink + edit AppArmor profile + reload + test | Add one line to manifest, wsh lock |
| Revoke a capability | Remove symlink + edit profile + reload + verify | Remove line, wsh lock |
| Audit what an agent did | ausearch + custom parsing | wsh audit |
| New agent, different permissions | New user + new symlinks + new profile + new audit rules | wsh setup codex with a different manifest |
| Verify nobody tampered with logs | Not possible without custom tooling | wsh audit verify (SHA-256 hash chain) |
2. What you can’t do at all with standard tools
The comparison above is generous. It assumes standard tools can approximate what wsh does. In reality, there’s an entire class of policy that no combination of standard Unix tools can express.
The reason is architectural: tools like AppArmor, SELinux, and auditd operate at the kernel level. They see binaries, file paths, and network sockets. They have no visibility into what a command means — its arguments, its intent, its application-layer semantics.
Semantic policies: impossible without wsh
| Policy | Standard tools | wsh |
|---|---|---|
Allow git push but deny git push --force | ✗ Kernel sees /usr/bin/git executing. Can’t inspect arguments. | ✓ git.push capability, push_force is a separate grant. |
Allow git clone only from github.com/your-org/* | ✗ Kernel sees a TCP connection to port 443. Can’t inspect the URL. | ✓ network.hosts allowlist in the manifest. |
Allow curl but only to specific API endpoints | ✗ Same problem. AppArmor can allow or block all network access. No URL filtering. | ✓ network.hosts restricts which domains are reachable. |
| Allow sending email but only to specific recipients | ✗ Completely invisible to the kernel. Email addresses are application data. | ✓ Manifest defines allowed recipients as capability parameters. |
Block shell evasion: eval, $(cmd), backtick substitution | ✗ These are shell interpreter features. The kernel doesn’t see them — it only sees the final execve() call. | ✓ wsh parses the command string, detects evasion patterns, and blocks before execution. |
Allow npm install but deny npm publish | ✗ Same binary, different subcommand. Kernel can’t distinguish. | ✓ Separate capabilities for each subcommand. |
| Allow file writes only within the project directory | ∼ AppArmor can do path-based restrictions, but can’t handle symlink tricks or relative path traversal reliably. | ✓ paths.project_root with canonicalisation and symlink resolution. |
The fundamental distinction: AppArmor controls what programs can run. wsh controls what programs can do.
This isn’t a criticism of AppArmor or SELinux — they’re excellent at what they do. But they were designed for a world where programs are trusted and the concern is privilege escalation between users. AI agents are a fundamentally different threat: a program running as you, with your permissions, that needs to be constrained at the semantic level.
3. The macOS problem
Everything above assumed Linux. On macOS — where a large proportion of developers (and AI agents) actually run — the situation is worse.
| Capability | Linux | macOS |
|---|---|---|
| Mandatory Access Control | AppArmor, SELinux | No equivalent. macOS has App Sandbox, but it’s designed for GUI apps distributed via the App Store — not for restricting CLI tools. |
| Kernel audit of command execution | auditd with execve rules | praudit / OpenBSM exists but is poorly documented, difficult to configure, and widely considered unusable for this purpose. |
| Restricted shell | rbash | rbash exists but agents bypass $SHELL entirely — Codex and Claude Code hardcode /bin/zsh -c. |
| Process sandboxing | namespaces, seccomp, cgroups | sandbox-exec (deprecated since macOS 10.15, undocumented, may be removed in any future release). |
| Network filtering per process | iptables / nftables with cgroup matching | pf exists but has no per-process or per-user filtering. Network extensions require a signed app bundle. |
macOS simply does not provide the primitives needed to restrict AI agents at the OS level. You can’t write an AppArmor profile because AppArmor doesn’t exist. You can’t use sandbox-exec because Apple deprecated it. You can’t filter network per-process because pf doesn’t support it.
wsh works identically on both platforms. Same policies, same manifests, same audit system. Install once, enforce everywhere.
The bottom line
There are three levels of AI agent security today:
- Nothing — the agent runs as you with full access. This is the default for every major AI coding agent.
- DIY with OS tools — restricted users, MAC profiles, audit rules. Hours of setup, Linux-only, no semantic policy, brittle maintenance. Most people never attempt this.
- wsh — two-minute setup, cross-platform, semantic policy enforcement, tamper-proof audit, cryptographically signed policies.
wsh isn’t competing with AppArmor — it’s filling a gap that didn’t exist until AI agents started running shell commands on our machines. In fact, our companion project warrant-box uses kernel-level isolation (Linux namespaces, seccomp, and AppArmor) alongside wsh to provide defence in depth: wsh handles semantic policy at the application layer, while warrant-box enforces hard containment at the kernel layer. They’re complementary, not competing.