agentafk
Configuration

Configuration Overview

How agent-afk resolves configuration: the ~/.afk/ directory layout, config tiers, and the full afk.config.json schema.

agent-afk keeps all state under ~/.afk/ — completely independent of ~/.claude/. You can delete ~/.claude/ entirely and afk still runs.

Directory layout

~/.afk/
  config/
    afk.env              # API keys and env overrides — written by `afk login` or `afk config env set`
    afk.config.json      # structured config — edited with `afk config set` or by the agent
    mcp.json             # MCP server registry
  state/                 # $AFK_STATE_DIR overrides this tier
    sessions/            # session-store sidecars
    todos/               # todo-panel data
    transcripts/         # autosaved REPL session transcripts
    daemon/              # per-instance daemon state
  agent-framework/       # local telemetry and operational data
  skills/                # user-authored or generated skills
  plugins/               # installed plugins and marketplace caches
  logs/
  cache/

The root directory is controlled by AFK_HOME (default ~/.afk/). Set it to an absolute path to relocate everything at once — useful on shared machines or when you want a project-isolated state tree.

Config resolution tiers

Configuration is layered. Higher tiers win over lower ones.

TierSourceWins over
1 (highest)AFK_SYSTEM_PROMPT env vareverything
2afk.config.json — searched in order: <cwd>/afk.config.json~/.afk/config/afk.config.json → legacy ~/.afk.config.jsonAFK.md
3 (lowest)AFK.md — searched in order: <cwd>/AFK.md~/.afk/AFK.mdnothing

The built-in prompt is always present. Any overlay you set is appended after it under an # Operator configuration header — it adds to the base, never overwrites it.

Environment variables override matching fields in afk.config.json. For example, AFK_MODEL overrides model. See Environment Variables for the full list.

.env file loading

Before env vars are read, loadEnvConfig() loads dotenv files in this order (first occurrence wins, no override):

  1. <cwd>/.env — per-repo overrides
  2. ~/.afk/config/afk.env — user-scope, the canonical place for API keys
  3. Legacy ~/.afk.env — back-compat

The afk login wizard writes keys to ~/.afk/config/afk.env.

Inspecting and editing config

afk config           # human-readable dump of resolved model, provider, API key presence
afk config --format json   # machine-readable JSON
afk doctor           # validates keys, paths, and provider connectivity

Edit config without opening an editor:

afk config set model opus            # write afk.config.json
afk config env set AFK_EFFORT high   # write afk.env
afk config env set ANTHROPIC_API_KEY # secrets prompt for a masked value

See Editing Configuration for the full command reference, the agent-vs-human sensitivity tiers, and the config_get/config_set tools the agent uses to tune its own settings.

--dump-prompt <path> on any command writes the fully-resolved system prompt (framework base + operator overlay) to a file, with provenance noted in a header (e.g. framework+afk-md:/path/to/AFK.md).

Overriding home

AFK_HOME=/opt/my-afk afk config

AFK_HOME must be an absolute path and must not be /. The runtime enforces this at startup.

What's not shared with Claude Code

AFK's state directories (~/.afk/) are fully independent of Claude Code's ~/.claude/. Plugin state for the plugin surface writes to ~/.claude/agent-framework/ independently — no shared state.


afk.config.json schema

afk.config.json is a JSON file (no JSONC comments in the runtime-parsed copy; the .example ships with comments for documentation only). All fields are optional; unset fields fall through to env vars or built-in defaults.

Top-level fields

{
  "model": "sonnet",
  "maxTokens": 4096,
  "temperature": 1.0,
  "systemPrompt": "Always run the test suite before declaring a task done. Prefer small, reversible commits.",
  "updatePolicy": "notify",
  "autoResumeOnUsageLimit": true,
  "bgSummaries": false,
  "maxSummaryCallsPerSession": 200,
  "enableShellHooks": true
}
FieldTypeDefaultDescription
modelstring"medium" (CLI) / "sonnet" (Telegram)Default model — varies by surface. CLI commands resolve via getModel(), defaulting to the medium capability tier (Claude Sonnet on an unconfigured install; rebinding models.medium changes the CLI default). Surfaces that read loadConfig().model directly instead — notably the Telegram bot — fall back to the literal DEFAULT_CONFIG.model ("sonnet"), so rebinding medium alone does not affect them. Accepts tier names (small/medium/large), fixed-identity aliases (sonnet, haiku, opus, fable), and full provider-native ids (gpt-4o, mlx-community/Qwen3-30B-A3B-4bit). Env override: AFK_MODEL. Migration: AFK_MODEL=sonnet now pins the fixed Sonnet identity rather than following a rebound medium tier.
maxTokensnumber4096Deprecated / inert — not read by the generation path. Use AFK_MAX_OUTPUT_TOKENS / --max-output-tokens to cap per-response output.
temperaturenumber1.0Sampling temperature. Env override: AFK_TEMPERATURE.
systemPromptstringOperator overlay appended to the framework base prompt (tier 2). When set, AFK.md is not loaded. Env override: AFK_SYSTEM_PROMPT (tier 1, wins over this).
updatePolicy"notify" | "auto" | "off""notify"Controls how AFK handles available CLI updates. notify prints a message; auto applies automatically; off disables checks.
autoResumeOnUsageLimitbooleantrueWhen true, AFK pauses and retries automatically when your API subscription hits its hourly limit, instead of showing a 429 error.
bgSummariesbooleanfalseWhen true, the REPL summarizes running background jobs and shows them in /bgsub:list. Caution: this sends transcript data to the Anthropic API — don't enable it in air-gapped or sensitive environments.
maxSummaryCallsPerSessionnumber200Session-wide budget for background summarizer LLM calls. Clamped to [1, 500]. Has no effect unless bgSummaries is true.
enableShellHooksbooleanfalseMaster switch for shell-command hooks. Must be set in a user-global file (~/.afk/config/afk.config.json); project-local values are silently ignored to prevent cloned repos from auto-executing scripts. Set it with afk config set enableShellHooks true — it is human-gated, so the agent's config_set tool cannot flip it.
enablePluginHooksbooleanfalseMaster switch for Claude Code plugin-contributed hooks (<plugin>/hooks/hooks.json). Independent of enableShellHooks. Must be set in a user-global file; human-gated (the agent's config_set cannot flip it). See Plugin-contributed hooks.

models — per-tier model bindings

Bind specific models to the small, medium, and large quality tiers used by skills and subagent dispatch. Each slot accepts a bare model id string or an object with optional provider credentials.

{
  "models": {
    "small": "claude-haiku-4-5-20251001",
    "medium": "claude-sonnet-5",
    "large": {
      "id": "claude-opus-5",
      "name": "big",
      "provider": "anthropic",
      "baseUrl": "http://localhost:8080",
      "apiKey": "sk-local"
    }
  }
}
FieldTypeDescription
smallstring | objectModel for cheap/fast work (e.g. classification, summarization). Env override: AFK_MODEL_SMALL.
mediumstring | objectModel for general tasks. Env override: AFK_MODEL_MEDIUM.
largestring | objectModel for heavy reasoning or long-context work. Env override: AFK_MODEL_LARGE.

When using the object form, each slot accepts:

Sub-fieldTypeDescription
idstringProvider-native model id.
namestringHuman-readable alias for this slot (cosmetic).
providerstringProvider name: "anthropic", "openai", or "chatgpt-oauth" (forces per-slot ChatGPT-subscription OAuth — see Models & Providers). Inferred from id when omitted.
baseUrlstringEndpoint base URL for this slot (e.g. a local shim).
apiKeystringAPI key for this slot's endpoint.

autoRouting — per-surface auto-routing

Controls whether each surface runs in "auto-routing" mode. When enabled for a surface, the agent automatically routes work without waiting for explicit confirmation. Accepts a boolean per surface key.

{
  "autoRouting": {
    "interactive": false,
    "chat": true,
    "telegram": true,
    "daemon": true
  }
}
FieldTypeDescription
interactivebooleanEnable auto-routing in the REPL (afk interactive).
chatbooleanEnable auto-routing for one-shot afk chat calls.
telegrambooleanEnable auto-routing in the Telegram bot surface.
daemonbooleanEnable auto-routing in the headless daemon.

Env override: AFK_AUTO_ROUTING=true sets all four surfaces simultaneously.


daemon — headless daemon defaults

Configures default task and optional worktree pruning for afk daemon.

{
  "daemon": {
    "task": "/review the most recent pr",
    "taskId": "default",
    "worktreePrune": {
      "enabled": true,
      "cron": "0 4 * * *",
      "maxAgeDaysClean": 14,
      "maxAgeDaysDirty": 30,
      "scope": "all"
    }
  }
}
FieldTypeDefaultDescription
taskstringSlash command or prompt the daemon runs on each scheduled invocation.
taskIdstring"default"Identifier for this task instance; used in telemetry and daemon state files.
worktreePrune.enabledbooleantrueWhether the daemon prunes stale AFK-managed worktrees on its cron schedule.
worktreePrune.cronstring"0 4 * * *"Cron expression controlling when pruning runs (5-field format).
worktreePrune.maxAgeDaysCleannumber14Worktrees with no uncommitted changes older than this many days are removed.
worktreePrune.maxAgeDaysDirtynumber30Worktrees with uncommitted changes older than this many days are removed.
worktreePrune.scopestring"all"Which worktrees to consider. "all" prunes all AFK-managed trees; other values may scope to a subset.

interactive — REPL defaults

Controls worktree and UX behavior for afk interactive.

{
  "interactive": {
    "worktreeAutoname": true,
    "worktreeBranchPrefix": "afk/",
    "worktreeBase": "origin/main",
    "suggestGhost": true
  }
}
FieldTypeDefaultDescription
worktreeAutonamebooleantrueWhen true, the first non-slash user message in a session started with afk i --worktree triggers a cheap haiku call to derive a kebab-case slug; the worktree dir and branch are renamed in place. Set false to keep the timestamp-based name. Env override: AFK_WORKTREE_AUTONAME.
worktreeBranchPrefixstring"afk/"Namespace prepended to AFK-managed worktree branch names (<prefix><slug>). Set to "" to drop the prefix. Validated at config-read time to reject ---prefixed values and shell metacharacters. Env override: AFK_WORKTREE_BRANCH_PREFIX.
worktreeBasestringremote defaultBase git ref for worktrees created with --worktree. Default is the remote's default branch (e.g. origin/main), fetched fresh. Set to HEAD to base on the local checkout. Env override: AFK_WORKTREE_BASE. CLI override: --worktree-base.
suggestGhostbooleantrueMaster toggle for REPL ghost-text suggestions. Set false to disable all ghost-text. Env override: AFK_SUGGEST_GHOST.

hooks — shell-command lifecycle hooks

The hooks block maps lifecycle event names to arrays of matcher groups. Each group runs one or more shell commands when the event fires and the optional tool-name matcher matches.

Prerequisites: enableShellHooks: true must be set in a user-global config file (~/.afk/config/afk.config.json). To also admit hook definitions from project-local files, additionally set allowProjectHooks: true in a user-global file.

{
  "enableShellHooks": true,
  "allowProjectHooks": false,
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "bash",
        "hooks": [
          {
            "type": "command",
            "command": "~/.afk/hooks/pre-bash-check.sh",
            "timeout_ms": 5000
          }
        ]
      },
      {
        "matcher": "/^write_/",
        "hooks": [
          { "type": "command", "command": "~/.afk/hooks/write-guard.sh" }
        ]
      }
    ],
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "~/.afk/hooks/on-session-start.sh",
            "timeout_ms": 3000
          }
        ]
      }
    ]
  }
}

Event keys

EventFires whenPayload fields (stdin JSON)Surfaces
PreToolUseBefore a tool call is dispatched. The hook can block the call.hook_event_name, session_id, tool_name, tool_inputAll
PostToolUseAfter a tool call completes successfully.hook_event_name, session_id, tool_name, tool_output (omitted when undefined)All
SessionStartAt the start of a session.hook_event_name, session_idAll
SessionEndWhen a session finishes.hook_event_name, session_id, reasonAll
SubagentStartWhen a subagent session is forked.hook_event_name, session_idAll
SubagentStopWhen a subagent session finishes. hookSpecificOutput.additionalContext injects text into the parent session.hook_event_name, session_idAll
StopAfter a main-session turn completes (end of one agent turn). Dispatched from the REPL turn loop only — subagents do not fire it. hookSpecificOutput.additionalContext is stashed and prepended to the next turn's prompt — the same next-turn delivery contract as UserPromptSubmit below, but fired from the turn-completion boundary rather than the submission boundary.hook_event_name, session_idREPL only
UserPromptSubmitWhen the user submits a prompt in the REPL, after shell injection and forward-manifest stitching but before runTurn. Handler can return injectContext (hookSpecificOutput.additionalContext) to prepend text to the prompt.hook_event_name, session_id, promptREPL only
PreCompactBefore context compaction runs. trigger is "manual" (via /compact command) or "auto" (threshold-based, reserved for future wiring).hook_event_name, session_id, trigger (null when unset)All
PostToolUseFailureAfter a tool call throws an error. Distinct from PostToolUse — only one fires per call. tool_input is intentionally not forwarded to shell hooks to avoid exposing sensitive inputs to arbitrary scripts.hook_event_name, session_id, tool_name, errorAll

Every event's payload also includes cwd and transcript_path (the latter is null when no transcript file is tracked).

A Stop hook's output reaches the model. Because Stop fires after every REPL turn, a Stop shell hook that returns hookSpecificOutput.additionalContext feeds that text into the next turn's prompt — the same channel UserPromptSubmit uses. This is the general injectContext primitive (see src/agent/hooks.ts) applied to a config-driven shell hook; it introduces no new trust boundary, since it only fires when you have already opted a Stop hook in under the enableShellHooks gate below.

Matcher group fields

FieldTypeDescription
matcherstringOptional. Omit or "*" to match any tool name. Exact string for an exact match. /regex/flags for a regex match (e.g. "/^write_/" matches all write tools). Only relevant for PreToolUse and PostToolUse.
hooksarrayOne or more hook definitions to run when this group matches.

Hook definition fields

FieldTypeDescription
type"command"Must be "command".
commandstringShell command to execute. Receives a JSON payload on stdin.
timeout_msnumberOptional timeout in milliseconds.

Exit-code protocol

Exit codeMeaning
0Continue. Optional JSON stdout may carry { "decision": "block", "reason": "…" } to block, or { "hookSpecificOutput": { "additionalContext": "…" } } for context injection. Effect depends on the event — see SubagentStop, Stop, and UserPromptSubmit in the Event keys table above.
2Block the operation. stderr becomes the reason shown to users.
otherNon-blocking error — logged, operation continues.

Trust gates

FieldScopeDescription
enableShellHooksUser-global onlyMaster switch. Without it, no shell hooks run regardless of what hooks defines.
allowProjectHooksUser-global onlyWhen true, hook definitions from project-local afk.config.json files are admitted. Defaults to false — without this, a cloned repo's hook definitions are silently dropped even when enableShellHooks is on.
enablePluginHooksUser-global onlyWhen true, hooks contributed by installed plugins (<plugin>/hooks/hooks.json) are discovered and run. Independent of enableShellHooks — plugin hooks are third-party code and get their own opt-in. Defaults to false.

Plugin-contributed hooks

AFK discovers and runs hooks shipped by installed plugins, matching Claude Code's plugin-hook layout so plugins are portable across both runtimes.

  • Location. Any installed plugin under ~/.afk/plugins/ that ships a <plugin>/hooks/hooks.json (alongside its .claude-plugin/plugin.json manifest) contributes hooks. Both the flat and marketplace-cache layouts are scanned. Plugin hooks merge after your own config hooks.
  • Trust gate. Plugin hooks run only when enablePluginHooks: true is set in a user-global file — a separate opt-in from enableShellHooks, because a plugin's hooks.json executes third-party code. When plugin hooks.json files are present but the gate is off, AFK emits a one-line warning naming how to enable them (it never runs them silently). Set it with afk config set enablePluginHooks true. Because the two gates are independent, turning enableShellHooks off does not disable plugin hooks — to run no third-party hooks, leave enablePluginHooks unset (or set it to false).
  • Supported types. Only command-type hooks run. Claude Code's other hook types (http, mcp_tool, prompt, agent) are skipped with a warning that names the unsupported type.
  • Environment. Plugin hook commands receive CLAUDE_PLUGIN_ROOT (the plugin's install directory) and CLAUDE_PROJECT_DIR, so commands like python3 "${CLAUDE_PLUGIN_ROOT}/hooks/on_start.py" resolve. The same secret redaction as shell hooks applies — no API keys or tokens are forwarded.
  • Scope. Discovery covers user-global installed plugins only. Project-scoped and bundled plugin hooks, inline hooks in plugin.json, and the non-command hook types are not yet honored.

On this page