# Tool Permissions (/agent/permissions)

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

## Approval [#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 `shell` 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 [#capabilities]

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

| Capability | Tools | Default |
| --- | --- | --- |
| Read | `read`, `ls`, `find`, `grep`, `web_search`, `web_fetch`, `memory_read`, `memory_list`, `skill_read`, `skill_list` | Allowed |
| Write | `write`, `edit`, `memory_write`, `skill_write` | Prompts |
| Exec | `shell` | Prompts |

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 [#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. `shell` 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 [#project-policy]

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

```toml
[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
```

| `default` | Effect |
| --- | --- |
| `read-only` | The default. MCP tools and built-in reads available; built-in writes and exec go through the gate |
| `deny` | Locks down all MCP tools. Anything in `allow` is still exposed |
| `allow` | Exposes everything not in `deny` |

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

The shell tool is named `shell`; it used to be `bash`, and that name is no longer recognised.
Rename `bash` to `shell` wherever you name the tool: `allow`, `deny`, hook matchers and a
subagent's `allowed_tools`. An old `deny = ["bash"]` matches nothing. The `jan` CLI warns about
each such `[tools]` entry when a run starts (the desktop app does not read `[tools]`), and every
surface refuses a subagent whose `allowed_tools` still says `bash`. `jan mcp serve --tool bash`
says so too.

## The shell sandbox [#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:

| Surface | Sandbox | Can it be changed? |
| --- | --- | --- |
| Desktop chat agent | Always on | No. If no sandbox can be established, `shell` is withheld rather than run unconfined |
| `jan` CLI and console | **Off by default** | Yes, 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.

<Callout type="warning">
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.
</Callout>

### Turning it on [#turning-it-on]

Three levels, most specific first:

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

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

```toml
# ~/.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:

```json
"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 `shell` entirely.

### Running a command outside the sandbox [#running-a-command-outside-the-sandbox]

Some everyday commands write outside the project: `cargo build` fills `~/.cargo/registry`, and
`pip`, `npm` and `go` write their caches under your home directory. Inside the sandbox these fail
with a read-only file system error. When that happens to a command the task needs, the agent can
ask to rerun it **outside** the sandbox. It sets `escalate` on the shell call and gives a one-line
reason, and you are asked:

```
shell wants exec outside the sandbox
$ cargo build
why cargo needs to write ~/.cargo/registry to fetch crates
> Allow once
  Deny
```

An approved command runs with your own access, like a command you typed. The minimal environment
(below) still applies. Escalation is deliberately stricter than every other prompt:

- **It is never auto-approved.** Running without `--safe` approves tools that run *inside* the
  sandbox, not a request to leave it, so the prompt still appears.
- **There is no "allow always".** Each escalation is answered on its own.
- **Where nobody can answer, it is refused.** That covers a headless `jan cli agent run` reading
  from a pipe or CI, and a subagent whose parent cannot be asked. The command does not run at all,
  and the model is told how you could enable it.
- **The desktop never escalates.** Its chat sandbox is throwaway by design.
- An escalated command runs outside the sandbox, so `~/.jan` is **not** hidden from it. A command
  that names `~/.jan` outright is refused before anyone is asked, but that check only reads the
  command text: a command that reaches the folder indirectly (`cd ~ && cat .jan/config.toml`)
  gets past it, and could read your provider keys. Read the command before you approve it.

For unattended runs that accept the risk, allow escalation without a prompt. Nobody then reads
the command, so any escalated command can reach everything you can, `~/.jan` included:

```bash
jan cli agent run --sandbox --allow-escalation "..."
```

```toml
# ~/.jan/projects/<slug>/agent.toml
[tools]
allow_escalation = true
```

This drops only the escalation prompt. With `--safe`, a shell command still needs your approval
whether or not it escalates, so you are asked once (about running it outside the sandbox) rather
than not at all. Adding `escalate` never gets a command past an approval it would otherwise need.

### Which shell runs commands [#which-shell-runs-commands]

On macOS and Linux the `shell` tool runs `bash` (or `sh` where there is none). On Windows it runs
PowerShell, never a bash: the sandbox is an AppContainer, and Git Bash's MSYS runtime cannot start
inside one. Jan uses the first of these that it finds:

1. PowerShell 7 (`pwsh`), when it is installed under Program Files. The sandbox can only start
   programs from folders all apps may read, so a `pwsh` from the Microsoft Store, scoop, a zip in
   your profile or a dotnet tool is skipped in favor of 5.1.
2. Windows PowerShell 5.1, which ships with Windows.
3. `cmd.exe`.

The model is told which shell it has, so it writes PowerShell or cmd syntax when that is what
runs. Hooks and plugin tools use the same shell. To force a specific shell, set
`JAN_AGENT_SHELL` to its full path.

### The shell environment [#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:

```toml
# ~/.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]

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:

| Surface | How to set network access | Default |
| --- | --- | --- |
| Desktop chat agent | The chat-screen tools toggle (`agent_tools`). Per session, in the UI | Off |
| `jan cli` code-screen agent | `[tools].allow_network` in the project's `agent.toml` | On |

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.

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

## Subagents [#subagents]

A [subagent's](/agent/subagents) 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 [#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](/agent/run-modes).

<Callout type="info">
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.
</Callout>
