Skip to content

Security Model

jai is a casual sandbox. This page explains what it protects, what it does not, and where its boundaries are. Read this before deciding whether jai is sufficient for your use case.

Integrity vs confidentiality

jai is primarily concerned with protecting your file system outside of the directories where you run agents. Depending on the mode, it is concerned with one or two things:

  • Integrity — Can the jailed process destroy or modify files it shouldn't? jai's overlay and mount isolation address this.
  • Confidentiality — Can the jailed process read files it shouldn't? This depends entirely on the mode.

Casual mode primarily provides integrity. Strict mode provides both integrity and confidentiality. Bare mode provides integrity with weaker confidentiality.

Another area of concern is whether jailed processes can attack the system through other processes. To guard against that, jai hides your $XDG_RUNTIME_DIR (where dangerous sockets often live), and runs processes in an isolated PID namespace. However, there are plenty of other ways to attack the system, particularly if you grant jailed code access to your Xorg or other servers on the system. Truly malicious code may be able to escape jai indirectly, though so far most AI agent horror stories involve storage destruction.

Casual mode

Casual mode mounts your home directory as a copy-on-write overlay. Writes (except for the directory where you invoked jai and below) go to $HOME/.jai/<name>.changes and do not modify originals. The current working directory and any subdirectories are writable as usual. /tmp and /var/tmp are private. The rest of the filesystem is read-only.

What it protects against:

  • Accidental or runaway deletion of files outside granted directories
  • Rogue rm -rf ~ or similar destructive commands (they will destroy the directory in which you ran jai and any granted directories, but not the rest of your home directory).
  • Agents modifying config files, dotfiles, or data in your home directory

What it does not protect:

  • Confidentiality. The process runs as your user and can read anything you can read — SSH keys, API tokens, browser data, credential files. The --mask option and default blacklists hide some sensitive dotfiles, but casual mode exposes everything readable by default. You cannot enumerate and mask every sensitive file.
  • Granted directories. The current working directory (and any -d directories) have full read/write access. A jailed process can destroy everything in those directories.
  • Network access. jai does not restrict network access. A jailed process can exfiltrate data, make API calls, or connect to remote services.
  • UI interactions. Jailed programs can interact with your terminal, and if you grant them access to your Xorg session (which happens by default in casual mode unless you mask or delete .Xauthority), can see your cut buffer or even send keystroke events to other windows.

Strict mode

Strict mode runs the jailed process as the unprivileged jai system user with an empty private home directory. Granted directories are exposed through id-mapped mounts.

What it adds over casual mode:

  • UID-based confidentiality. Because the process runs as a different user, it cannot read files that are only accessible to your UID. This blocks access to most of the sensitive data in your home directory.
  • Empty home directory. No dotfiles, no cached credentials, no browser profiles — the jailed process starts clean.

What it still does not protect:

  • World-readable files. Files with permissions like 644 outside your home directory are still readable (e.g., /etc/passwd, system configs). This is inherent to UID-based isolation without a full container.
  • Granted directories. As with casual mode, directories you grant via -d are fully read/write.
  • Network access. No network restrictions.

Bare mode

Bare mode uses an empty private home directory but runs as your user. It exists primarily as a fallback for NFS home directories, where id-mapped mounts are not supported.

Compared to strict mode:

  • Same empty home directory
  • Same private /tmp and /var/tmp
  • But the process runs as your UID, so it can read any file accessible to you outside the home directory
  • No confidentiality benefit beyond hiding the home directory contents

Use bare mode only when strict mode is not available due to NFS constraints.

PID namespace

All three modes use a private PID namespace (via CLONE_NEWPID). This means:

  • Jailed processes cannot kill or send signals to processes outside the jail
  • Jailed processes cannot ptrace processes outside the jail
  • ps inside the jail only shows jailed processes

This is a meaningful defense against a rogue process trying to interfere with your other running programs.

File masking

The --mask option applies only to casual jails and provides a measure of confidentiality for your most sensitive files, if you remember to enumerate them. The option creates an overlay "whiteout" file to hide a specific path in your home directory. The default .defaults file masks common sensitive paths such as .ssh and .gnupg. You can only mask files relative to your home directory, because only your home directory is an overlay.

Limitations:

  • Masks are only applied when the overlay mount is first created. If you add new mask directives, run jai -u first.
  • --unmask only reverses a previous --mask in the same config chain — it cannot unmask files already masked in an existing overlay. (To do that, you have to stop everything with jai -u, then go into $HOME/.jai/*.changes and delete the whiteout files hiding the underlying files you want to expose again.)
  • If you mask a parent directory, --unmask on a child path has no effect.
  • In casual mode, masking is defense-in-depth, not a guarantee. Because the process runs as your user, it may be able to read the underlying files through other paths if the overlay is misconfigured.
  • Beware of backup files. E.g., .netrc may be masked by default, but if your editor creates a backup file .netrc~, or a temporary file .#.netrc#, those will not be masked by default.

Overlayfs caveats

  • The overlay work directory ($HOME/.jai/*.work) occasionally accumulates root-owned files that the user cannot delete. Run jai -u to clean these up.
  • If extended attributes on the changes directory get out of sync, the overlay may fail to mount. Creating a fresh changes directory can resolve this.
  • Overlayfs requires extended attribute support in the "upper" directory. NFS does not support extended attributes, so casual mode overlays do not work if your .changes directories are on NFS. See below to get around this.

NFS limitations

NFS home directories cannot use:

  • Strict mode — id-mapped mounts are not supported on NFS. You have to fall back to bare mode.

  • Casual mode overlays — overlayfs requires extended attributes in the "upper" directory.

    To get around this limitation you can make $HOME/.jai a symbolic link to a local directory you own on a local disk, or you can set the JAI_CONFIG_DIR environment variable to relocate configuration files from $HOME/.jai. Finally, you can keep your configuration files in your home directory and move just the storage directories off NFS via the --storage configuration option.

Environment variable filtering

jai filters environment variables matching patterns like *_TOKEN, *_KEY, *_PASSWORD, etc. from the jailed environment. The default .defaults file sets these patterns. You can add more by editing the .defaults file or with the --unsetenv option. You can also selectively re-expose variables with --setenv.

This is a best-effort measure. Environment variable filtering cannot catch every secret — programs may store credentials in files, keyrings, or other locations. You could potentially filter everything with unsetenv * and then whitelist individual environment variables by putting the following in a configuration file:

    unsetenv *
    setenv HOME
    setenv PATH
    setenv SHELL
    setenv TERM
    setenv EDITOR
    setenv LANG
    setenv PAGER

(If you say setenv VAR, as opposed to setenv VAR=VALUE, it simply exposes the existing value of the environment variable, overriding a previous unsetenv.)

What jai is not

jai is not:

  • A hardened container runtime (like Docker with seccomp and AppArmor profiles)
  • A virtual machine
  • A replacement for gVisor, Kata Containers, or other strong isolation tools
  • A multi-tenant isolation boundary
  • A defense against a determined adversary with local access

When to use a real container or VM

Use a proper container or VM when:

  • You are running untrusted code from unknown sources (not just AI agents you chose to install)
  • You need strong multi-tenant isolation between users or workloads
  • You need network-level isolation (jai does not restrict network access)
  • You need syscall filtering (seccomp) or mandatory access control (AppArmor, SELinux)
  • The consequences of a sandbox escape are severe (production data, financial systems, medical records)

jai is designed for a different situation: you have a tool you broadly trust but want to limit the damage from bugs, mistakes, and overly eager file operations. It makes the common case safer without the overhead of a full container or VM.