# Safety
Three layers, in increasing order of how much they actually protect you.
**Path jail.** Every path a tool is given is resolved with
`filepath.EvalSymlinks` and refused if it lands outside the working root,
including through a symlink that points out of the tree. Cheap, and it catches
a lot.
**Approval prompts.** Read-only tools run unattended. `bash`, `edit_file`,
`write_file`, `web_search` and `web_fetch` ask, with "yes / no / always this exact one / always this tool" and
session memory. `/allowed` lists what you have agreed to. `--allowedTools` and
`--disallowedTools` write the same decisions down ahead of time, in Claude
Code's rule syntax — see [using it in place of
`claude`](in-place-of-claude.md) — and a deny rule there is checked
before everything else, `-yolo` included.
Shell commands are parsed with `mvdan.cc/sh/syntax` so the prompt can tell you
what a command will actually run — `git status; rm -rf ~` is shown as running
both `git` and `rm`. **This is not a security boundary**, and Codey does
not pretend otherwise: a command containing `$(...)`, backticks, or an
interpreter with `-c` cannot be analysed at all, so it is marked opaque and
"always allow this exact command" is refused for it. String-matching
allowlists for shell commands do not work; this exists to make the prompt
specific, not to make the decision for you.
[Claude Code's permission rules](claude-code-settings.md) sit in front of all
of that. Its deny rules hold in every mode, `-yolo` included, which makes them
the one part of this layer you can lean on: a call they refuse does not run.
Its allow rules are matched with the same parser, one simple command at a
time, and an opaque command matches none of them — but the warning above
applies to them with full force. `Bash(find:*)` allows `find . -exec rm {} +`,
and no parser will tell you otherwise.
Hooks sit outside all three layers, deliberately: a command in `"hooks"` runs
on every event it is attached to, without a prompt, because you put it in your
own config rather than the agent asking for it. The path jail is what keeps the
agent from adding one — the config lives in `~/.config/codey/`, outside any
working root you would sensibly point Codey at — but the flip side is that
anything able to write that file has arranged to run commands here. `-no-hooks`
is the switch that says no for one run. MCP servers are the same bargain: each
is a program started with your environment when the session opens, and only
its tool calls go through the prompts. `-no-mcp` is theirs. Only the servers
in Codey's own config are started; a repository's `.mcp.json` is never read.
**A sandbox.** This is the layer that actually contains a mistake, and the
Codey cannot provide it for you. The honest recipe on Linux:
```bash
podman run --rm -it \
-v "$PWD":/work:Z -w /work \
-e ANTHROPIC_API_KEY \
docker.io/library/golang:1.26 \
/work/codey -C /work -yolo
```
**Decide about egress explicitly.** The container above has network access,
because Codey needs to reach the API — which means the agent's `bash`
has it too. That is the line between "broke my repo" and "read my `.env` and
made an HTTP request". If that matters for the tree you are pointing it at:
- Treat every secret reachable from inside the container as exposed.
- Use a token scoped to nothing else, and a clone rather than your working tree.
- For real isolation, run Codey on the host and put only `bash` in a
network-less namespace. That is not implemented here.
`-yolo` skips every prompt. It is correct inside a container or against a
throwaway clone, and wrong everywhere else. The eval runner sets it, because it
is headless and operates on a temp directory it created.