agent-locker 0.1.0-alpha.2

A sandbox for running coding agents with restricted filesystem access.
agent-locker-0.1.0-alpha.2 is not a library.

Agent Locker

A sandbox for running coding agents with restricted filesystem access. The whole filesystem is made read-only via Landlock; only a small, explicit set of paths is writable.

agent-locker itself only ever launches one of the built-in agents (claude, codex, opencode) — you cannot ask it to run an arbitrary top-level command. This does not stop a launched agent from spawning other processes; see Threat model for what the sandbox does and does not protect.

Requires Linux 5.13+ with Landlock enabled, and currently x86_64 only.

Threat model

agent-locker constrains filesystem writes and nothing else. It is an integrity boundary — it stops unwanted modifications — not a confidentiality or network boundary. Understand the difference before relying on it, especially if you opt into flags like --dangerously-skip-permissions (see Configuration), which lean on this sandbox as the safety net.

What it protects: files and directories outside the writable set cannot be modified, created, deleted, or have their metadata changed. An agent — or anything it runs — cannot overwrite your home directory, system files, other projects, or (by default) a git worktree's shared parent git directory.

What it does not protect:

  • Reading. The entire filesystem stays readable. Secrets such as ~/.ssh, ~/.aws/credentials, API tokens, and other projects' source are all readable by the agent.
  • Network. The Landlock backend restricts only the filesystem; outbound network access is unrestricted. Anything readable can be exfiltrated.
  • Process execution. The whole filesystem remains executable. A launched agent can spawn arbitrary subprocesses and run any binary on the system. "Only the built-in agents are supported" refers solely to what agent-locker launches, not to what the agent does afterward.

/dev/tty is writable because interactive agents need it. agent-locker installs a small seccomp filter that blocks the TIOCSTI ioctl, so the agent cannot inject keystrokes into the controlling terminal to run commands in the launching shell after it exits — independent of kernel version (the Linux 6.2 TIOCSTI hardening covers only newer kernels). Other terminal ioctls are unaffected.

If you need confidentiality, network isolation, or execution confinement, run the agent in a VM or container with no network and no access to your secrets; agent-locker alone does not provide them.

Usage

agent-locker [-C DIR] <agent> [args...]
  • -C, --context-dir DIR — the main writable project directory (defaults to the current directory).
  • --allow-parent-git / --no-allow-parent-git — grant or deny write access to a git worktree's parent git directory without prompting (see Git worktrees).
  • <agent> must be one of claude, codex, or opencode. Any following arguments are passed through to the agent.
agent-locker claude               # claude in the current directory
agent-locker -C ./project claude  # claude scoped to ./project
agent-locker codex --some-flag    # pass extra arguments through to codex

What's writable

Every invocation grants write access to:

  • the context directory,
  • /tmp,
  • /dev/null and /dev/tty — the only device nodes any agent needs to write (verified via strace). Reads elsewhere (e.g. /dev/urandom) still work because the rest of the filesystem remains readable.

Everything else — including the home directory, /dev, and /run/user — is read-only unless listed below.

Agent-specific writable paths

Each agent additionally allows its own config/cache directories:

Agent Program Extra writable paths
claude claude ~/.claude (see below)
codex codex ~/.codex
opencode opencode ~/.opencode, ~/.local/share/opencode, ~/.cache/opencode

claude normally stores its main config in ~/.claude.json, but it writes that file atomically — to a temp file in the same directory, then rename()d over the target — which needs permission to create and remove files in the file's parent directory. Granting that on $HOME would make the whole home directory writable, defeating the sandbox, and a file-level Landlock rule on ~/.claude.json cannot authorize the rename. So for the claude preset, agent-locker sets CLAUDE_CONFIG_DIR=~/.claude, which relocates the config to ~/.claude/.claude.json — inside the already-writable config directory, where the atomic write succeeds. On first sandboxed run an existing ~/.claude.json is copied in so history, onboarding, and MCP approvals carry over.

Because of this, a sandboxed claude reads and writes ~/.claude/.claude.json while a claude launched normally (outside agent-locker) still uses ~/.claude.json; the two diverge after the initial copy. To keep a single config across both, set CLAUDE_CONFIG_DIR=~/.claude in your own environment so the normal claude uses the same relocated file.

Agents run in their default mode — agent-locker does not force flags like --dangerously-skip-permissions. Use the config file to opt in.

Git worktrees

In a git worktree, the .git entry is a file pointing at the parent repository's git directory, which lives outside the worktree (e.g. working in …/suricata/review, a worktree of …/suricata/main, the parent git directory is …/suricata/main/.git). Writing there — to commit, create branches, stash, etc. — needs an explicit grant, because that directory is shared with the main checkout and every other worktree.

agent-locker does not grant it by default. When it detects that the context directory is a worktree, it:

  • grants write access if --allow-parent-git was passed,
  • leaves it read-only if --no-allow-parent-git was passed,
  • otherwise applies the allow_parent_git config default (see Configuration) if set,
  • otherwise prompts you (and leaves it read-only if there is no terminal to prompt, e.g. when run non-interactively).

The command-line flags always override the config default.

Ordinary (non-worktree) repositories are unaffected: their .git lives inside the context directory and is writable as part of it.

Context roots

Sometimes you want a wider writable area than the current directory — for example, a development tree where you move between sibling checkouts and expect the agent to write across all of them. The context_roots config key lists such directories:

context_roots = ["~/oisf/dev"]

When you launch an agent without -C and the current directory is at or below a configured root, that root becomes the context (the main writable project directory) instead of the current directory. So running anywhere under ~/oisf/dev — say ~/oisf/dev/suricata/src — scopes the sandbox to all of ~/oisf/dev.

  • The deepest (most specific) matching root wins, so you can list both ~/oisf and ~/oisf/dev and the latter applies when you are beneath it.
  • An explicit -C/--context-dir always overrides context roots.
  • Matching is by path component, so ~/oisf/dev does not match a sibling like ~/oisf/development.
  • Because the root becomes the context, git worktree detection (and the allow_parent_git prompt) applies to the root, not the current directory.

Configuration

Optional config file at $XDG_CONFIG_HOME/agent-locker/config.toml, falling back to ~/.config/agent-locker/config.toml when XDG_CONFIG_HOME is unset.

The top-level allow_parent_git key sets the default for a git worktree's parent git directory (see Git worktrees), so you are never prompted; --allow-parent-git / --no-allow-parent-git override it.

The top-level context_roots key lists directories that become the context whenever the agent is launched at or beneath them (see Context roots).

Each preset section may supply args, which are prepended to the preset's command line, before any arguments you pass on the command line. A missing config file is fine. Unknown keys are ignored so a config written for a different agent-locker version still loads; only a config that is not valid TOML is an error.

Exhaustive example

# Default for granting write access to a git worktree's parent git directory.
# Omit to be prompted on detection; the --allow-parent-git / --no-allow-parent-git
# flags override this.
allow_parent_git = false

# Directories that become the context (the main writable project directory)
# whenever the agent is launched at or beneath them. See "Context roots" below.
context_roots = ["~/projects/evebox"]

# Arguments are prepended to the preset's command line, before any arguments
# passed on the command line. Each section is optional; omit a section to run
# that preset with no extra arguments.

[claude]
# Skip claude's interactive permission prompts.
args = ["--dangerously-skip-permissions"]

[codex]
# Bypass codex's own approval/sandbox prompts.
args = ["--dangerously-bypass-approvals-and-sandbox"]

[opencode]
# opencode takes no special arguments by default; add your own here.
args = []