agentafk
Configuration

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).

ModeBehaviorSet by
defaultPath containment + path-approval prompts for out-of-root file access. No per-tool flow.REPL and Telegram default
planRead-only investigation; write-class tools are blocked until you approve a plan./plan in the REPL
autonomousAFK-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
bypassPermissionsDisables 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:

ChoiceEffect
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 planningStays 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   grep

The 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:

ChoiceEffect
OnceAllow this one call; the grant is revoked immediately afterward.
SessionAdd the path to the granted roots for the rest of this session (in memory only).
PersistLike Session, and also write the grant to disk so future sessions skip the prompt.
DenyBlock 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 root

The 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.json

Persisted 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-store

Plus 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/Firefox

Symlinks 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-linerspython -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 use read_file / write_file / edit_file (which carry their own per-call approval) instead. Lift just this denylist with AFK_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-dir will not lift it. Read the file with read_file, or set AFK_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/gh directory (wider than the typed floor's single hosts.yml file). ~/Library/Application Support is also wider here than the typed floor — bash restricts the whole directory, while read_file/grep/glob restrict 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-permissions

Bypass 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 mode

Three 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_telegram is always allowed.
  • Remote channel — elicitations and a remote /abort are 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, mode 0600) 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

SurfaceDefault modeNotes
REPL (afk i)defaultOut-of-root file access prompts inline; cycle to bypassPermissions with Shift+Tab to disable containment.
Telegram botdefaultSame as the REPL; prompts are pushed to your chat.
afk chatdefaultNon-interactive — out-of-root access can't be approved, so it is denied; pass --dangerously-skip-permissions for wide-open access.
afk daemonbypassPermissionsUnattended; containment off so no prompt can wedge the run.

Environment variables

VariableEffectDefault
AFK_DISABLE_PATH_APPROVAL1 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_GUARD1 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_GUARD1 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_DENYLISTComma-separated path globs the write_file tool always refuses, regardless of mode (including bypass).
AFK_READ_DENYLISTColon-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.

On this page