github akitaonrails/ai-jail v1.20.1

latest releases: v2.8.1, v2.8.0, v2.7.0...
one month ago

Fixed

macOS only, and all four share one cause: the profile canonicalizes every path
it emits, but seatbelt checks file-read-metadata on each component while
resolving one. Anything an allowed target is reached through was therefore
never named, so the grant did not apply to the path callers actually walk.
Reported and fixed by @xlordz (#124).

  • The sandbox could exec a binary it never vetted. v1.19.3 started resolving
    the command through its symlink chain, but the chain was canonicalized away
    before it reached the profile, so only the final target was named — never the
    PATH entry that execvp walks. An unnamed entry does not make execvp
    fail; it continues down PATH and runs the first match inside an
    already-readable prefix. Because the same release made /opt readable, a
    Homebrew copy would win: ai-jail resolved and granted one build of an agent
    and then silently ran a different one. Naming the entry closes it.

  • DNS and TLS both failed with --network. /etc, /var and /tmp are
    symlinks into /private, so their targets were allowed and unreachable at the
    same time — name resolution never worked, and the CA bundle at
    /etc/ssl/cert.pem could not be read even though /private/etc was granted.

  • No Node script could run. With no directory above an allowed path
    stat-able, anything calling realpath(3) on its entry point died first, the
    mounted project included: EPERM: operation not permitted, lstat '/Users'.
    node -e worked, which is why this stayed hidden until an agent ran a script,
    a test runner, or any CLI with a #!/usr/bin/env node shebang.

  • Claude Code could not read its own scratch directory. The
    /private/tmp/claude-<uid> grant was write-only. Node's recursive mkdir
    takes EEXIST from the kernel and then stats the path to confirm it is a
    directory; that denied stat is what surfaced as
    EEXIST: file already exists, mkdir '/tmp/claude-<uid>' on the second launch
    in a project.

Every added rule is file-read-metadata on a node already on the way to
something allowed, emitted before the deny-list so an explicit --deny-path
still wins, plus one uid-scoped read that mirrors the write grant already
present. No rule is removed and no file-write* is added. A blanket
(allow file-read-metadata (subpath "/")) was deliberately rejected: it also
fixes name resolution, but it overrides the profile's own deny-list and lets
stat answer for ~/.ssh and ~/.aws.

Linux is unaffected. There the command's symlink node is bind-mounted at its own
path, so it was always named.

Checksums (SHA256)

f0d974f29a0ae37c0ca4fcfee6b3ca92ee3220e31e4e0a013b5e5a99c9851962  ai-jail-linux-x86_64.tar.gz
b2c473ff982e7c0c520a289115e3018fd2c6b65dad574e6ef1b183ee6bda34f4  ai-jail-macos-aarch64.tar.gz

Install

brew tap akitaonrails/tap && brew install ai-jail
yay -S ai-jail-bin
cargo install --locked ai-jail

Don't miss a new ai-jail release

NewReleases is sending notifications on new releases.