Docs
Jan Agent
Tool Permissions

Tool Permissions

The agent asks before it changes anything. This page covers how that works and how to change it.

Approval

Reads are free. Writes and shell commands stop and ask:


write src/cli.rs
y yes, once
a yes, and don't ask again this session
n no

For write and edit you see a diff first. For bash you see the exact command.

a grants that capability for the rest of the session. Nothing is written to disk, so a new session starts cautious again - a grant can't leak into tomorrow.

Capabilities

Every tool maps to one capability, and approval applies at that level rather than per tool:

CapabilityToolsDefault
Readread, ls, find, grep, web_search, web_fetch, memory_read, memory_list, skill_read, skill_listAllowed
Writewrite, edit, memory_write, skill_writePrompts
ExecbashPrompts

So approving a on a shell command allows further shell commands, not just that one. That's the trade for not being asked twenty times during a test loop - and the reason to prefer y when you only meant one.

Path checks

Tools that take a path have it checked against the project root, so a write outside the project is refused before it happens. bash has no path argument and runs in the project root; a command you approve can still reach outside, which is why plan mode exists as the hard boundary.

A symbolic link is followed only while it stays inside the project (or the session scratch). Links that point back into your own tree - the ones every node_modules is full of - work normally; a link out of the project is refused, so it can't be used to smuggle a read or a write past the check above.

Project policy

The project's agent.toml (in ~/.jan/projects/<slug>/) sets what a project exposes:


[tools]
# read-only | deny | allow
default = "read-only"
# Exposed even under deny; the deny list wins over everything
allow = []
deny = []
# Write tools are opt-in
allow_write = []
# Whether the sandboxed shell can reach the network
# allow_network = true
# Whether shell commands run under OS confinement at all
# sandbox = true

defaultEffect
read-onlyThe default. MCP tools and built-in reads available; built-in writes and exec go through the gate
denyLocks down all MCP tools. Anything in allow is still exposed
allowExposes everything not in deny

deny always wins over allow. A tool in both lists is denied.

The shell sandbox

Jan can run shell commands under OS confinement - bubblewrap on Linux, Seatbelt on macOS, AppContainer on Windows. Confined, a command can write only the project and its scratch directory, and reaches the network only if allowed. Reads are looser: the CLI keeps $HOME readable so git and ssh credential helpers work ([tools].allow_home_read, set it to false to close that), while the desktop masks it entirely.

While the sandbox is on, Jan's own home, ~/.jan, is hidden from every tool and from the shell, even when you run jan from your home directory. It holds config.toml (provider keys and hooks) and each project's agent.toml, memory and threads, so no approval can open it: file tools refuse the path, ls, find and grep leave it out, and the shell sees an empty directory. The agent still reaches memory and skills through its memory_* and skill_* tools. A git worktree Jan created under ~/.jan/worktrees/ stays reachable, because it is the project being worked on.

Whether it is on depends on the surface:

SurfaceSandboxCan it be changed?
Desktop chat agentAlways onNo. If no sandbox can be established, bash is withheld rather than run unconfined
jan CLI and consoleOff by defaultYes, see below

The CLI runs unconfined because it is a coding agent in your own project, started from your own terminal - the same trust make, git push, or any other tool you run there already has - and confinement breaks the toolchains projects actually build with often enough to be the wrong default. Note that approval prompts are also off by default, so out of the box nothing gates a shell command at all - --safe, --sandbox, or both are how you change that.

⚠️

With the sandbox off, a shell command you approve runs with exactly your access. Nothing confines it to the project. If that isn't the trade you want, turn it on.

Turning it on

Three levels, most specific first:


jan --sandbox # this session only
jan --no-sandbox # this session only, overriding a setting below


# ~/.jan/projects/<slug>/agent.toml - every session in this project
[tools]
sandbox = true


# ~/.jan/config.toml - for every project you work on
sandbox = true

A flag beats the project file, which beats the global setting, which beats the default. Every level works in both directions, which is why --no-sandbox exists: without it, turning confinement on permanently would leave no way to run a single command without it.

jan cli agent status reports what a run would actually do:


"sandbox": { "enabled": false, "backend": "bubblewrap" }

backend is the confinement that would be used. none means none is available on this machine - with enabled: true that combination is what withholds bash entirely.

The shell environment

The shell does not inherit your full environment. Jan starts it from a minimal base - PATH, HOME, TMPDIR, locale, and the Windows system keys - so a secret in your environment (OPENAI_API_KEY, SSH_AUTH_SOCK, and the like) never reaches a command the model runs, whether or not the sandbox is on. This is deliberate, but it also means a command that needs a variable you rely on (a proxy setting, git config, a toolchain home) will not see it.

Two keys widen it, without giving up the default:


# ~/.jan/projects/<slug>/agent.toml (per project) or ~/.jan/config.toml (all projects)
[tools]
# Host variables the shell may inherit. Exact names or a "*" wildcard.
env_passthrough = ["SSH_AUTH_SOCK", "GIT_*", "http_proxy", "https_proxy"]
# Explicit values, set regardless of your environment.
[tools.env_set]
RUST_LOG = "info"

  • env_passthrough copies the named host variables in if they are set. The two scopes merge: the global list and the project list both apply.
  • A secret-looking name (containing KEY, TOKEN, SECRET, PASSWORD, PASSWD, CREDENTIAL, HEADERS or AUTHORIZATION, case-insensitive) is never copied by a wildcard - so a broad GIT_* cannot leak a GIT_TOKEN, and OTEL_* cannot copy OTEL_EXPORTER_OTLP_HEADERS, which holds an API key when a launcher configures telemetry. To pass one deliberately, name it in env_set, which is not filtered.
  • env_set wins over env_passthrough and over the global value for the same key. Under the sandbox, TMPDIR/TMP/TEMP are always pointed at the run's scratch directory regardless.

Network access

Network access is a property of the sandbox, so it applies when the sandbox is on. Unconfined, a command has whatever network access you do. The two surfaces default differently:

SurfaceHow to set network accessDefault
Desktop chat agentThe chat-screen tools toggle (agent_tools). Per session, in the UIOff
jan cli code-screen agent[tools].allow_network in the project's agent.tomlOn

The split is deliberate. The desktop chat agent is ephemeral, so it stays offline unless you opt in. The CLI is a coding agent in your own project, where one that cannot curl, git fetch, or install a package would be largely useless.


# Take a CLI agent offline
[tools]
allow_network = false

Subagents

A subagent's tools are narrowed, never widened. For a saved subagent an allowlist further restricts its own; for a one-off it is its toolset. Either way a subagent reaches a subset of what the parent could, so delegation can't be used to escape this policy.

Turning the prompts on

Approval prompts are opt-in: by default every call above is auto-approved. Start the session with jan --safe to be asked instead. See run modes.

Auto-approval only removes the prompt. The path checks and [tools] deny all still apply, as does the shell sandbox where it is on - but on the CLI it is off by default, so auto-approved shell commands run with your own access unless you turn it on.