Permissions
How agent-afk contains tool access: the default path-approval prompt, bash restrictions, bypass mode, autonomous mode, and the env vars that tune them.
agent-afk does not prompt before each tool call — there is no per-tool "allow this
bash command?" flow. The agent runs bash, reads and writes files, fetches URLs, and calls MCP
servers without asking each time. This is intentional: AFK is built for unattended work, where a
permission prompt with no human in front of it just wedges the session.
The one boundary it does enforce by default is path containment: a file tool that reaches outside the session's working directory asks for approval before it runs. Everything else about permissions is a knob layered on top of that.
Permission modes
A session runs in one permission mode, set by the permissionMode key in afk.config.json
(or the --dangerously-skip-permissions flag, which forces bypassPermissions).
| Mode | Behavior | Set by |
|---|---|---|
default | Path containment + path-approval prompts for out-of-root file access. No per-tool flow. | REPL and Telegram default |
plan | Read-only investigation; write-class tools are blocked until you approve a plan. | /plan in the REPL |
autonomous | AFK-specific mode (agent-afk PermissionMode value). Default containment plus a risk gate that refuses high-risk/irreversible actions tree-wide, a system-prompt addendum telling the model to work autonomously on reversible operations, and a Telegram channel that pushes each turn's terminal state so you can supervise from your phone. | /afk in the REPL |
bypassPermissions | Disables containment and path approval entirely — read/write any path with no prompt. | Shift+Tab from plan, the --dangerously-skip-permissions flag, or afk daemon |
The interactive default is default, not bypass. Plan and autonomous modes are mutually
exclusive with each other (they share the one permissionMode field). autonomous is an
agent-afk–local value; it is not an Anthropic SDK permission level. The internal SDK-level values
dontAsk, auto, and acceptEdits are not user-facing modes and are not surfaced in the REPL.
Cycling modes with Shift+Tab
In the REPL, Shift+Tab advances through a fixed three-stop ring without leaving the keyboard:
default → plan → bypassPermissions → default → …autonomous (AFK) is intentionally excluded from the ring — entering it activates
non-idempotent machinery (Telegram push-budget reset, abort-watcher, ledger channel swap) that
must not fire on a transient keypress. If the session is already in autonomous, Shift+Tab exits
AFK cleanly (running full teardown) and lands on default; the next press resumes the ring.
Leaving plan mode — exit_plan_mode escalation
When the model judges its plan ready, it calls the exit_plan_mode tool and the REPL presents
you with a picker:
| Choice | Effect |
|---|---|
| Approve — implement now (restore previous mode) | Restores the mode you were in before entering plan (typically default). Implementation begins on the next turn. |
| Approve — implement now (bypass mode) | Escalates to bypassPermissions for the implement turn — offered unless you were already in bypass before planning. |
| Keep planning | Stays in plan mode; the model continues refining. |
The permission flip is deferred to the post-turn drain boundary so the plan-mode gate stays
locked for the entire current model turn, closing a mid-turn TOCTOU window. This is
behaviorally identical to running /plan off yourself.
Path-access approval
In default mode, six typed file tools are contained to the session's granted roots:
read_file write_file edit_file list_directory glob grepThe initial granted root is the session's working directory (the cwd of the first turn). A tool
that targets a path inside a granted root runs with no prompt. A tool that targets a path
outside every granted root pauses and asks:
| Choice | Effect |
|---|---|
| Once | Allow this one call; the grant is revoked immediately afterward. |
| Session | Add the path to the granted roots for the rest of this session (in memory only). |
| Persist | Like Session, and also write the grant to disk so future sessions skip the prompt. |
| Deny | Block the call. |
The prompt is delivered through the active elicitation surface — inline in the REPL, or pushed to your phone over Telegram.
Pre-authorizing paths with /allow-dir
Rather than wait for a prompt, pre-authorize a directory in the REPL:
/allow-dir ~/Projects/shared-lib # grant read+write for this session
/allow-dir --revoke ~/Projects/shared-lib # revoke a previously granted rootThe session's initial working directory is a non-revocable base — /allow-dir --revoke cannot
strip it.
Persisted grants
Choosing Persist (or answering an out-of-root prompt with "persist") records the grant to:
~/.afk/config/permissions.jsonPersisted allow grants are replayed at the start of every future interactive session, so a path
you trust once stays trusted. A write-mode grant implies read access (write ⊇ read). The file is
read fail-soft: if it is missing or corrupt, AFK treats it as empty grants rather than locking you
out.
Forked subagents cannot answer a path prompt — they have no operator surface of their own. An out-of-root access in a subagent is auto-denied with a message telling the subagent to surface the requirement to its parent. Pre-authorize the path in the parent (or run in bypass) if a subagent needs wider access. See Subagents.
Protected paths
Below the grant system sits a credential floor that no mode lifts — not bypass, not an unconfined
session, not a forked subagent. read_file, grep, glob, and list_directory refuse these paths
outright:
~/.ssh ~/.aws ~/.gnupg ~/.config/gcloud
~/.afk/config ~/.npmrc ~/.docker/config.json
~/.git-credentials ~/.netrc ~/.config/gh/hosts.yml
~/.kube/config /etc/shadow /etc/sudoers /private/etc/master.passwd
~/.password-storePlus each browser's secret tree under ~/Library/Application Support (never the whole
directory — see below):
~/Library/Application Support/Google/Chrome
~/Library/Application Support/Chromium
~/Library/Application Support/BraveSoftware
~/Library/Application Support/Microsoft Edge
~/Library/Application Support/Arc
~/Library/Application Support/FirefoxSymlinks are dereferenced before the comparison, so a link into a protected directory is refused
too. Two deliberate exclusions: ~/.afk/state stays readable (subagents read todos, transcripts,
and skill inputs there), and ~/.afk/config/mcp.json is carved out as an exact file — the MCP
registry holds ${VAR} placeholders, not literal secrets, and agents need it to diagnose a server
that will not connect. The carve-out is exact: mcp.json.bak and anything else in that directory
stays blocked.
The browser list is deliberately narrower than the bash hook's floor over the whole
~/Library/Application Support directory (see Bash restrictions). Bash's
floor is grant-liftable — /allow-dir <path> drops it for a session — so flooring the whole
directory there costs nothing permanent. This floor has no such escape hatch: it is unconditional
and no operator, bypass mode, or forked subagent can ever lift a built-in entry, so it floors only
the actual secret material — Login Data (saved passwords), Cookies (session tokens), and Web Data (autofill, including payment cards) for Chromium-family browsers; logins.json and key4.db
for Firefox. A non-secret Application Support sibling, like an editor's settings.json, stays
readable. Safari's secrets live outside Application Support entirely (~/Library/Safari,
~/Library/Cookies) and are not covered by either surface today.
Add your own entries with AFK_READ_DENYLIST (colon-separated absolute paths; a leading ~/ is
expanded, but ~user/ is not — spell those out). Operator entries are matched before the
carve-out, so AFK_READ_DENYLIST=~/.afk/config/mcp.json re-protects the registry if yours holds
inline secrets. Built-in entries always apply and cannot be removed.
The same list backs the bash restrictions below, so on interactive surfaces (REPL, Telegram) a
credential path is protected whether the agent reaches for read_file or for cat — and bash
additionally floors a few wider, shell-only roots (see below). The typed-tool floor is unconditional
on every surface; the bash half is not — see Bash restrictions for the
headless exception.
The two halves match differently, and the difference is worth knowing. The typed tools resolve a
path before comparing it, so a symlink pointing into a protected directory is refused. The bash hook
scans the command text — it expands ~ and $HOME, but it cannot see through a symlink alias or a
path assembled from shell variables. So ln -s ~/.ssh /tmp/k && cat /tmp/k/id_rsa is refused by
read_file and not by the bash hook. That hook exists to stop the accidental cat, not a determined
one; the typed-tool floor is the real boundary.
Bash restrictions
Alongside path approval, a bash-restriction hook blocks two classes of command by default on interactive surfaces (REPL, Telegram) — no prompt, just a refusal that routes the model back to the typed file tools:
-
Interpreter one-liners —
python -c,python3 -c,node -e,ruby -e,perl -e,osascript -e,sh -c,bash -c,zsh -c,fish -c,lua -e, and the like. These are an easy way to bypass path containment, so they are blocked; the model is told to useread_file/write_file/edit_file(which carry their own per-call approval) instead. Lift just this denylist withAFK_DISABLE_BASH_INTERPRETER_GUARD=1.This check matches a fixed set of credential fragments against the command text, and those fragments are not anchored to your home directory — so an interpreter one-liner touching a project-local file that happens to share a spelling (a repo's own
.npmrc,.afk/config/) is refused too. Unlike restricted roots, this one is not grant-filtered:/allow-dirwill not lift it. Read the file withread_file, or setAFK_DISABLE_BASH_INTERPRETER_GUARD=1. -
Restricted roots — commands referencing sensitive paths. This is the same credential list the typed read tools refuse (see Protected paths above) plus shell-only additions: the whole
~/.config/ghdirectory (wider than the typed floor's singlehosts.ymlfile).~/Library/Application Supportis also wider here than the typed floor — bash restricts the whole directory, whileread_file/grep/globrestrict only the per-browser secret trees inside it (see Protected paths) — because bash's floor is grant-liftable (/allow-dir <path>drops it for a session) while the typed floor is unconditional and permanent. Each/etc/…entry is also matched as its/private/etc/…twin, and vice versa, since macOS reaches the same file by both names. A restricted path is allowed only when a granted root already covers it.
On headless surfaces (afk chat, afk daemon) both checks fail open. No grant manager is
wired there, so the restricted-root list is empty and the interpreter denylist has no approval path
to redirect the model to. Legitimate automation running python -c is not hard-blocked with no
recourse — but neither is cat ~/.ssh/id_rsa. Treat bash as unrestricted on those surfaces; the
typed file tools keep their unconditional credential floor either way.
AFK_FORCE_BASH_INTERPRETER_GUARD=1 opts headless flows back into the interpreter denylist only,
matched against the built-in credential-fragment signal (there are no grants to derive a root list
from). It does not restore the restricted-root check, so a plain cat of a credential path still
passes. For strict containment in unattended runs, use an OS-level sandbox — sandbox-exec,
Landlock/bubblewrap, or a container — rather than this hook.
Bypass mode
Bypass mode (permissionMode: 'bypassPermissions') turns the containment off entirely: filesystem
tools may read and write any path with no confirmation, and the path-approval prompt never
fires.
In the REPL the primary keyboard entry point is Shift+Tab (advances the ring
default → plan → bypassPermissions). From the command line, the equivalent is the
--dangerously-skip-permissions flag on both afk i and afk chat:
afk chat "run the pending migrations and fix any broken model references" --dangerously-skip-permissionsBypass lets the agent read and write anywhere on the machine with no prompt. Use it only on a
machine and account you trust. To keep changes contained while still running unprompted, prefer an
isolated git worktree (afk i -w) over bypass.
afk daemon runs in bypass mode by default — there is no human present to answer a prompt, so a
path-approval dialog would simply wedge the run. Bypass does not affect ask_question: the
model choosing to ask you something is a separate axis from filesystem containment.
Autonomous mode & remote supervision
/afk puts an interactive session into autonomous mode, designed for "start it at the keyboard,
walk away, supervise from your phone":
/afk # enable autonomous mode
/afk off # return to default modeThree things switch on together:
- Posture — a system-prompt addendum tells the model to act autonomously on reversible work and stop at one-way doors.
- Risk gate — a PreToolUse hook classifies each tool call and blocks high-risk actions
(destructive bash, writes escaping the workspace) while allowing safe and medium-risk ones. The
gate applies to the whole session tree, including subagents.
send_telegramis always allowed. - Remote channel — elicitations and a remote
/abortare bridged through a per-session ledger, so a watching Telegram daemon can answer a prompt or stop the run from your phone. Ledger records are signed with a per-session HMAC key (~/.afk/state/sessions/<id>/session.key, mode0600) and verified before the REPL acts on them. The keyboard remains live alongside the remote channel — there is no dependency on the daemon being up.
Surface defaults
| Surface | Default mode | Notes |
|---|---|---|
REPL (afk i) | default | Out-of-root file access prompts inline; cycle to bypassPermissions with Shift+Tab to disable containment. |
| Telegram bot | default | Same as the REPL; prompts are pushed to your chat. |
afk chat | default | Non-interactive — out-of-root access can't be approved, so it is denied; pass --dangerously-skip-permissions for wide-open access. |
afk daemon | bypassPermissions | Unattended; containment off so no prompt can wedge the run. |
Environment variables
| Variable | Effect | Default |
|---|---|---|
AFK_DISABLE_PATH_APPROVAL | 1 skips both the path-approval and bash-restriction hooks entirely — wide-open file access for headless flows that need it. | 0 (hooks on) |
AFK_DISABLE_BASH_INTERPRETER_GUARD | 1 skips only the interpreter-eval denylist; the restricted-root check stays active. Wins over the force flag. | 0 (denylist on for interactive) |
AFK_FORCE_BASH_INTERPRETER_GUARD | 1 forces the interpreter denylist on even on headless surfaces, where it otherwise fails open. The restricted-root check stays open there regardless. | 0 (headless fails open) |
AFK_WRITE_DENYLIST | Comma-separated path globs the write_file tool always refuses, regardless of mode (including bypass). | — |
AFK_READ_DENYLIST | Colon-separated absolute paths added to the credential floor above (a leading ~/ is expanded) — refused by the typed read tools on every surface, and by bash on interactive surfaces only (headless fails open). Adds only; built-ins cannot be removed. | — |
See also
- Editing Configuration — config sensitivity tiers; why credentials and safety keys are human-only.
- Unattended Runs — running the daemon and Telegram supervision safely.
- Subagents — why forked subagents auto-deny out-of-root access.