jaim is a macOS port of jai, an
ultra-lightweight jail for AI command-line tools. It uses macOS
Seatbelt
sandbox profiles to restrict what sandboxed processes can access, so you
can run AI assistants without giving them free rein over your system.
THIS IS STILL VERY EXPERIMENTAL. USE AT YOUR OWN RISK
With that said, I believe this is at a point of usefulness where having adventural individuals take it for a test run is beneficial for feedback and community building purposes.
I tested casual mode but as #1 mentions changes to modes need to be updated. Casual actually acts like jai's strict. As such, reconciling this may produce breaking changes. Such changes will be noted.
This was created using jai's code as a starting point and using this project to test Steve Yegge's Gas City.
My part of the code was created through agentic engineering using Gas City. Feedback on quality is welcome.
I will do my best to keep up with the changes made in the jai project.
jaim command runs command with the following default policy:
-
command has full read/write access to the current working directory.
-
In casual mode (the default), command has read-only access to your home directory, with sensitive files masked (SSH keys, cloud credentials, browser data, shell history, etc.).
-
In bare mode, command sees an empty private home directory at
~/.jaim/<jail>.home/instead of your real home; the real home is denied entirely by the sandbox. The private home is writable and persists across invocations, so tool state (shell history, session caches) is retained. Useful for running a tool with your user credentials but with no ambient dotfiles, history, or config bleeding through from the real home. -
In strict mode, command has no access to your home directory at all, except the current working directory and any explicitly granted paths.
-
/tmpis a fresh private directory per invocation: the shared system/tmp//private/tmpis denied,TMPDIRis set to the private dir, and the dir is removed when jaim exits. -
The rest of the filesystem is read-only.
-
Network access is unrestricted.
-
Sensitive environment variables (tokens, passwords, API keys) are stripped automatically.
-
Three security modes -- casual, bare, and strict -- to balance convenience and isolation.
-
Named sandboxes -- multiple independent sandbox configurations that don't see each other's state.
-
Directory grants -- grant read-only or full access to additional directories with
--dir,--rdir, and related options. -
File masking -- deny access to specific paths within your home directory (
.ssh,.aws,.gnupg, etc.) even in casual mode. -
Environment filtering -- automatically strips credentials and secrets from the sandboxed environment. Configurable with
--setenvand--unsetenv. -
Per-command configuration -- configuration files in
~/.jaim/<command>.conflet you set different policies for different tools. -
Shell scripting -- source custom bash functions into sandboxed shells via
~/.jaim/.jaimrc. -
No root required -- unlike the Linux version, jaim uses the unprivileged macOS sandbox API.
- macOS on Apple Silicon (arm64)
- Xcode Command Line Tools -- install with
xcode-select --install. This provides theclang++compiler. jaim needs a C++23-capable compiler; Xcode 16 / Apple Clang 16 or newer is sufficient. If the build fails with errors about unknown C++ features, update Xcode from the App Store. - Optional:
autoconfandautomake-- only needed for the autotools build (brew install autoconf automake). - Optional:
pandoc-- only needed to regenerate thejaim.1man page fromjaim.1.md(brew install pandoc).
Nothing from Homebrew is required for this path -- Xcode Command Line
Tools is sufficient. The included GNUmakefile builds jaim without
autotools:
make
make installThis installs to ~/.local/bin by default (no sudo required).
Make sure ~/.local/bin is in your PATH:
export PATH="$HOME/.local/bin:$PATH"To install to a different prefix:
make PREFIX=/usr/local install # requires sudoIf you prefer the autotools workflow, first install the prerequisites:
brew install autoconf automakeThen build:
./autogen.sh # only from a git checkout
./configure
make
make installAfter building, initialize your configuration:
jaim --initThis creates ~/.jaim/ with default configuration files (.defaults,
default.conf, default.jail, .jaimrc). You can edit these to
customize the sandbox behavior.
Run a command inside the sandbox:
jaim claude
jaim bash
jaim your-ai-tool --some-flagRun with no arguments to get a sandboxed shell:
jaim-m, --mode casual|bare|strict Set the sandbox mode
-d, --dir DIR Grant full access to DIR
-r, --rdir DIR Grant read-only access to DIR
-F, --file FILE Grant write access to a specific FILE
-j, --jail NAME Use a named sandbox
-D, --nocwd Don't grant access to the current directory
-C, --conf FILE Use a specific configuration file
-u Tear down sandbox state (see below) and exit
--mask FILE Deny access to $HOME/FILE
--unmask FILE Undo a previous --mask
--setenv VAR[=VALUE] Set an environment variable
--unsetenv VAR Remove an environment variable (supports wildcards)
--help Show full help
--version Show version information
Run Claude Code. On first run, jaim creates ~/.jaim/claude.conf
with the grants Claude Code needs (write access to ~/.claude/ for
session state and user-level settings, plus ~/.claude.json for its
global config), so casual-mode home protection stays intact while
Claude Code can still persist its state:
jaim claudeGive Claude Code access to an extra data directory on top of the defaults:
jaim -d ~/data claudeCasual mode makes $HOME read-only. Tools other than the ones
jaim ships presets for (see Using jaim with coding agents below)
need their state paths granted explicitly. Use -d for
directories and -F for individual files:
jaim -d ~/.config/mytool -F ~/.mytoolrc mytoolRun in strict mode with no home directory access:
jaim -m strict bashUse a named sandbox to keep state separate:
jaim -j project-a claude
jaim -j project-b claudeReset a sandbox's private home (wipes ~/.jaim/project-a.home/ and
any stray jaim.* scratch dirs in $TMPDIR; leaves your .conf /
.jail config files alone):
jaim -u -j project-aWithout -j, jaim -u sweeps every *.home/ under ~/.jaim/.
jaim exists to jail coding agents, and casual mode is read-only on
$HOME so a confused or prompt-injected agent cannot delete your
files. Agents still need to write something in $HOME — session
history, user-level config, MCP server settings — so each supported
agent ships a per-command config file that grants those specific
paths and nothing else.
Claude Code (claude): ~/.jaim/claude.conf ships with jaim.
Grants ~/.claude/ (projects, CLAUDE.md, settings.json, MCP
servers) and ~/.claude.json (global config). Running jaim claude
picks it up automatically.
Other agents (aider, codex, cursor-cli, ...): create a
~/.jaim/<name>.conf modeled on claude.conf. Use sudo fs_usage -w -f filesys | grep <name> from another terminal while the tool
runs to discover the exact paths it writes, then add dir /
file directives for each one.
Configuration lives in ~/.jaim/ (or $JAIM_CONFIG_DIR):
| File | Purpose |
|---|---|
.defaults |
Base defaults included by other config files |
default.conf |
Default configuration (includes .defaults) |
default.jail |
Default sandbox settings (mode, etc.) |
.jaimrc |
Bash functions available in sandboxed shells |
claude.conf |
Per-command config for Claude Code (shipped) |
<name>.conf |
Per-command configuration |
<name>.jail |
Per-sandbox settings |
Configuration files accept the same directives as the command line (without the leading dashes). The most useful for tailoring a sandbox are:
| Directive | Purpose |
|---|---|
mode casual... |
Set the sandbox mode |
mask FILE |
Deny access to $HOME/FILE |
unmask FILE |
Undo a previous mask |
dir DIR |
Grant full access to a directory |
rdir DIR |
Grant read-only access to a directory |
file FILE |
Grant write access to a specific file |
setenv VAR=.. |
Set an environment variable |
unsetenv VAR |
Strip an environment variable (supports *) |
The file directive is atomic-write aware: it grants access to
FILE itself and to any FILE.<suffix> temp sibling used by
libraries like node's write-file-atomic or Python's atomicwrites.
This is tighter than granting the whole parent directory and lets
you unblock a single dotfile in the home root without exposing the
rest of $HOME.
To view the built-in defaults:
jaim --print-defaultsThe built-in defaults include an unsetenv list that strips
credentials (*_API_KEY, *_TOKEN, DATABASE_URL, etc.) from the
sandbox environment. A setenv VAR=VALUE line in any config file
(.defaults, default.conf, <name>.conf, etc.) unconditionally
overrides any prior unsetenv for VAR, including matching
wildcard patterns. In particular, a line like
setenv ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
expands ${ANTHROPIC_API_KEY} from the real environment at
config-parse time and passes the token into the sandbox, silently
defeating the default *_API_KEY strip. jaim will emit a warning
to stderr whenever a setenv VAR=... in a config file overrides an
unsetenv pattern (command-line --setenv never warns, since it
reflects explicit user intent).
jaim prevents accidents, not attacks. It is designed to stop an AI tool from damaging your system through bugs or careless commands -- not to contain a deliberately malicious or prompt-injected agent.
What jaim protects against:
- An agent running
rm -rfin the wrong place. - Unintended writes outside the current working directory (for example
to
~/.bashrcor~/.ssh/config). - Reads of masked sensitive files (SSH keys, cloud credentials, browser data, shell history).
- Inherited file descriptors smuggling sandbox-bypassing access into
the sandboxed program. jaim enumerates the child's open descriptors
with
proc_pidinfoand closes everything above stderr beforeexecve, so a caller that opened a file outside the sandbox cannot hand that FD to the sandboxed program as a back door around the path-based sandbox rules. - Cross-sandbox or unsandboxed-to-sandbox leaks through
/tmp. Each jaim invocation gets a fresh private scratch directory and the shared system/tmp//private/tmp//var/folderstrees are denied, so sandboxed processes cannot read files dropped by concurrent sandboxes or by unsandboxed processes, nor leave files there for anyone else to pick up after exit.
What jaim does NOT protect against:
- Data exfiltration over the network. Network access is
unrestricted in every mode, including
strict. Anything an agent can read -- the current working directory, git history, unmasked parts of your home directory -- it can POST to any host on the internet. Seatbelt does not block outbound connections, and jaim does not currently emit(deny network*)rules. - A compromised or prompt-injected agent. If an agent decides to ship the contents of your working directory to an attacker, no jaim mode will stop it.
- Secrets in the current working directory. jaim grants full
read/write access to the CWD. Anything sitting there --
.envfiles, API tokens, credentials in sibling git repos -- is readable by the agent and therefore exfiltratable.
- Use
strictmode to deny all home directory access. - Run jaim from a directory that contains only what the agent needs. Do not point jaim at a directory that holds secrets, credentials, or unrelated git repos.
- Add an outbound firewall (Little Snitch, LuLu, or pf rules) if you need containment against network exfiltration. jaim does not currently expose a flag to deny network.
- Treat jaim as a seatbelt, not a vault. For adversarial AI workloads, a VM or a separate user account gives stronger isolation.
jaim uses the macOS Seatbelt sandbox (sandbox_init(3)) to enforce
file access restrictions. When you run jaim command:
-
jaim reads your configuration files to determine the sandbox mode, granted directories, masked files, and environment filters.
-
It generates a Seatbelt profile -- a set of allow/deny rules in Apple's Scheme-like sandbox profile language -- based on your configuration.
-
It forks a child process, applies the sandbox profile with
sandbox_init(), sets up the environment, and execs the command. -
The parent process waits for the child and propagates its exit status.
The sandbox is enforced by the kernel. Once applied, a process cannot escape it or weaken its restrictions. Unlike containers or VMs, there is no filesystem overhead -- jaim processes see the real filesystem, just with access restrictions enforced by the kernel.
jaim is a macOS port of jai by David Mazieres. The original jai targets modern Linux (kernel 6.13+) and uses namespaces and overlayfs for isolation. jaim replaces those Linux-specific mechanisms with macOS Seatbelt sandbox profiles.
GNU General Public License v3 or later. See COPYING.