typeclaw 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +4 -0
  2. package/package.json +1 -1
  3. package/src/agent/index.ts +80 -8
  4. package/src/agent/live-subagents.ts +215 -0
  5. package/src/agent/plugin-tools.ts +60 -20
  6. package/src/agent/session-origin.ts +15 -0
  7. package/src/agent/subagents.ts +140 -3
  8. package/src/agent/system-prompt.ts +40 -0
  9. package/src/agent/tools/channel-reply.ts +24 -1
  10. package/src/agent/tools/channel-send.ts +26 -1
  11. package/src/agent/tools/spawn-subagent.ts +283 -0
  12. package/src/agent/tools/subagent-cancel.ts +96 -0
  13. package/src/agent/tools/subagent-output.ts +192 -0
  14. package/src/bundled-plugins/agent-browser/skills/agent-browser/SKILL.md +26 -0
  15. package/src/bundled-plugins/explorer/explorer.ts +103 -0
  16. package/src/bundled-plugins/explorer/index.ts +11 -0
  17. package/src/bundled-plugins/guard/index.ts +12 -1
  18. package/src/bundled-plugins/guard/policies/managed-config.ts +139 -0
  19. package/src/bundled-plugins/guard/policy.ts +1 -0
  20. package/src/bundled-plugins/operator/index.ts +11 -0
  21. package/src/bundled-plugins/operator/operator.ts +76 -0
  22. package/src/bundled-plugins/scout/index.ts +11 -0
  23. package/src/bundled-plugins/scout/scout.ts +94 -0
  24. package/src/channels/router.ts +32 -0
  25. package/src/cli/channel.ts +2 -45
  26. package/src/cli/init.ts +2 -45
  27. package/src/cli/model.ts +2 -1
  28. package/src/cli/ui.ts +95 -0
  29. package/src/config/config.ts +45 -12
  30. package/src/config/index.ts +3 -0
  31. package/src/cron/index.ts +3 -0
  32. package/src/cron/schema.ts +20 -0
  33. package/src/init/dockerfile.ts +156 -5
  34. package/src/init/index.ts +33 -0
  35. package/src/permissions/builtins.ts +23 -2
  36. package/src/plugin/define.ts +2 -0
  37. package/src/plugin/index.ts +2 -0
  38. package/src/plugin/types.ts +15 -22
  39. package/src/run/bundled-plugins.ts +6 -0
  40. package/src/run/channel-session-factory.ts +19 -0
  41. package/src/run/index.ts +56 -6
  42. package/src/server/index.ts +103 -0
  43. package/src/skills/typeclaw-claude-code/SKILL.md +273 -0
  44. package/src/skills/typeclaw-claude-code/references/auth-flow.md +135 -0
  45. package/src/skills/typeclaw-claude-code/references/stop-hook.md +99 -0
  46. package/src/skills/typeclaw-claude-code/references/tmux-driving.md +157 -0
  47. package/src/skills/typeclaw-config/SKILL.md +29 -26
  48. package/typeclaw.schema.json +6 -0
@@ -0,0 +1,99 @@
1
+ # Stop hook — schema and gotchas
2
+
3
+ Deep dive for the `Stop` lifecycle hook that powers the done-signal. Read it when the basic hook in `SKILL.md` isn't enough — when the transcript looks stale, the sentinel is malformed, or you need to extract intermediate tool calls from the JSONL.
4
+
5
+ ## What fires when
6
+
7
+ Claude Code supports several lifecycle hooks. The two relevant to delegation are:
8
+
9
+ - **`Stop`** — fires every time the _main_ agent finishes responding. This is per-turn, not per-session. A 5-turn conversation fires `Stop` five times. The "task is done" signal is just "the latest Stop, where claude's message looks like a result not a question" — that's the multi-turn decision loop in `SKILL.md`.
10
+ - **`SubagentStop`** — fires when a _sub-agent_ (Task tool, plan-mode sub-agents, etc.) finishes. Sub-agents are claude spawning claude. You don't typically need to handle this — the parent's `Stop` fires after its sub-agents are done. Configure it only if you want progress signals during a sub-agent-heavy turn.
11
+
12
+ Other hooks that exist (`PreToolUse`, `PostToolUse`, `Notification`, `SessionStart`, `SessionEnd`, `PreCompact`, etc.) are out of scope for this skill — they're useful for progress logging, command auditing, or session bookkeeping, but they're not the done-signal.
13
+
14
+ ## Stop event JSON schema
15
+
16
+ The hook command receives a single JSON object on stdin. Fields observed in current Claude Code (subject to upstream churn — the docs page is at `https://docs.anthropic.com/en/docs/claude-code/hooks`):
17
+
18
+ ```jsonc
19
+ {
20
+ "session_id": "abc123…", // The Claude Code session UUID
21
+ "transcript_path": "/root/.claude/projects/-tmp-cc-foo/abc123.jsonl",
22
+ "cwd": "/tmp/cc-foo", // Should match your worktree path
23
+ "permission_mode": "default", // or "plan", "bypassPermissions", etc.
24
+ "hook_event_name": "Stop", // Literal "Stop" for this event
25
+ "stop_hook_active": false, // True only while the hook itself runs
26
+ "last_assistant_message": "…", // The text of claude's just-finished turn
27
+ }
28
+ ```
29
+
30
+ Fields you actually use:
31
+
32
+ - **`last_assistant_message`** — your primary capture for the multi-turn decision loop. Read this from `sentinel.json`, classify (question / permission / result / spurious), act.
33
+ - **`transcript_path`** — points at the JSONL with the full conversation. Useful when `last_assistant_message` isn't enough.
34
+ - **`cwd`** — sanity check. If this isn't `/tmp/cc-<id>`, something is wrong with your tmux spawn (likely missing `-c`).
35
+ - **`session_id`** — useful for logging or if you want to correlate with the JSONL filename.
36
+
37
+ Fields you ignore:
38
+
39
+ - `permission_mode`, `stop_hook_active` — for hook-internal coordination, not delegation logic.
40
+
41
+ ### SubagentStop deltas
42
+
43
+ If you ever configure a `SubagentStop` hook, expect these additional fields:
44
+
45
+ ```jsonc
46
+ {
47
+ "agent_id": "def456…",
48
+ "agent_transcript_path": "/root/.claude/projects/-tmp-cc-foo/abc123/subagents/agent-def456.jsonl",
49
+ }
50
+ ```
51
+
52
+ The schema is otherwise the same. `agent_transcript_path` is a separate JSONL per sub-agent.
53
+
54
+ ## The transcript JSONL
55
+
56
+ `transcript_path` points at a JSONL file with one JSON object per line. Anthropic does not publish a formal schema — community tools (claudeoo, maury, serac) have reverse-engineered it. What you'll see:
57
+
58
+ - **`{ "type": "user", "message": { … } }`** — what you sent claude (or what the upstream parent sent, for sub-agents).
59
+ - **`{ "type": "assistant", "message": { "content": [ … ] } }`** — claude's response. `content` is an array of `{ "type": "text", "text": "…" }` and `{ "type": "tool_use", … }` objects.
60
+ - **`{ "type": "tool_use", "name": "Read", "input": { … } }`** — tool calls claude made.
61
+ - **`{ "type": "tool_result", "tool_use_id": "…", "content": "…" }`** — tool results.
62
+ - **`{ "type": "system", "subtype": "…" }`** — system events: `compact_boundary`, `turn_duration`, `stop_hook_summary`, etc.
63
+ - **`{ "type": "attachment", … }`** — file uploads or contextual attachments.
64
+
65
+ To extract claude's final text answer when `last_assistant_message` isn't enough:
66
+
67
+ ```sh
68
+ # Read every assistant-text content line from the JSONL
69
+ jq -r 'select(.type == "assistant") | .message.content[] | select(.type == "text") | .text' "$transcript_path"
70
+ ```
71
+
72
+ Filter further by timestamp or message-id if you only want the last turn.
73
+
74
+ ## Documented race conditions
75
+
76
+ Three known races, all from upstream Claude Code issues. The skill body's design avoids them by preferring the sentinel over the JSONL; this is the reasoning if you have to debug:
77
+
78
+ 1. **Stale transcript on Stop (#15813).** The `Stop` hook can fire before the last assistant message is flushed to the JSONL. If you read `transcript_path` immediately on hook fire, the last message may not be there yet. **Mitigation:** use `last_assistant_message` from the hook's stdin JSON as the primary capture; treat the JSONL as the backup, with a 1–2 second wait if it looks stale.
79
+ 2. **Missing transcript file (#20612, #30217).** Some users report `transcript_path` pointing at a file that doesn't exist, especially in multi-session or concurrent-worktree setups. **Mitigation:** capture `last_assistant_message` on every Stop and accumulate it yourself if you need the full history. Falling back to `tmux capture-pane -S -` is the last-resort path.
80
+ 3. **Inaccurate final token counts (#27361).** The JSONL has historically missed the final `message_stop` SSE event, causing `output_tokens` to be a mid-stream snapshot (sometimes undercounted by ~2x). **Mitigation:** don't rely on JSONL token counts for cost calculations; the Anthropic Console workspace usage is the authoritative source.
81
+
82
+ ## Permission prompts vs Stop
83
+
84
+ A subtle point that confuses the multi-turn decision loop: **permission prompts do not fire `Stop`**. When claude is waiting for a "Allow this command?" yes/no, the turn isn't over — the model is waiting for the _user_ (you) to type y/n into the TUI, not waiting for a new prompt. So:
85
+
86
+ - **Permission prompt appears** → no `Stop`, no `.done`, you keep polling.
87
+ - **You answer the prompt** (via `tmux send-keys "y" Enter`) → claude continues working, eventually finishes its turn → `Stop` fires.
88
+
89
+ This is why the multi-turn loop's classification is "ends with question mark / contains 'Do you want me to'" — it's looking for _content-level_ questions claude wrote as part of its response, not permission-tool prompts. Permission prompts don't reach `last_assistant_message`; they only appear in the pane.
90
+
91
+ If you need to detect permission prompts (to auto-answer them), `capture-pane` is the only signal. Look for the literal yes/no UI affordance at the bottom of the pane. This is risky to automate — you're answering on the user's behalf for an operation you can't see the full safety implications of. Default behavior in this skill: pause polling for the sentinel, look at the pane after the budget elapses without a `.done`, and if there's a permission prompt sitting there, surface to the user rather than auto-answering.
92
+
93
+ ## Things you must not do with the Stop hook
94
+
95
+ - **Do not set `matcher` to anything other than `"*"`.** The matcher filters by hook tool name; for `Stop`, there's no tool — `"*"` is the canonical "fire on every Stop". Other values may silently never match.
96
+ - **Do not put long-running commands in the hook.** The hook runs synchronously on the Claude Code main loop; a slow hook blocks the user's next prompt. Write the payload + touch a flag + exit. Anything heavier belongs in your polling loop, not the hook.
97
+ - **Do not skip the temp-file rename pattern.** Writing `sentinel.json` directly with `>` lets readers see partial JSON if they poll mid-write. Always `cat > sentinel.json.tmp && mv sentinel.json.tmp sentinel.json`.
98
+ - **Do not delete `transcript_path` from inside the hook.** The path is shared with `SessionEnd` and other lifecycle events; deleting it breaks downstream hooks.
99
+ - **Do not log the full hook payload to a place you don't control.** It contains `last_assistant_message`, which can contain anything claude said — including code, secrets the user pasted, or private context. Sentinel is fine (it's in `/tmp/`); piping to a shared log is not.
@@ -0,0 +1,157 @@
1
+ # Tmux driving — full reference
2
+
3
+ Deep dive for driving `claude` (interactive) through tmux. Read it when the main `SKILL.md` workflow hits an edge case: a hung session, a missing JSONL, an unexpected ANSI burst, a "tmux says session exists but claude isn't responsive" situation.
4
+
5
+ ## Why tmux
6
+
7
+ The agent process has no TTY. `claude` (interactive) is a TUI that uses raw terminal modes — it reads stdin one byte at a time with echo off, draws with ANSI escapes, and refuses to start if stdin/stdout aren't terminals. Piping into `claude` (`echo "prompt" | claude`) doesn't work; the CLI detects the absence of a TTY and either falls back to `-p` semantics (no plan mode) or fails outright.
8
+
9
+ `tmux` solves this by spawning the process with a pty allocated by tmux itself. You then drive the pty via `send-keys` (write) and `capture-pane` (read). The process believes it's running interactively because, from its file descriptor's point of view, it is.
10
+
11
+ ## Why `/tmp/cc-<id>` and not `workspace/cc-<id>`
12
+
13
+ The cwd of the spawned `claude` process must be the git worktree at `/tmp/cc-<id>/`, not the agent folder's `workspace/`. Three reasons:
14
+
15
+ 1. **Worktree-vs-scratch:** the `cc-<id>` directory is a real git checkout managed by `git worktree`, with refs in `/agent/.git/worktrees/cc-<id>/`. Putting it under `workspace/` would mean the agent folder contains a worktree of itself, which works mechanically but is recursive and confusing.
16
+ 2. **Claude's `.claude/settings.json` is read relative to cwd.** It must live at `/tmp/cc-<id>/.claude/settings.json` so claude picks up the per-task `Stop` hook.
17
+ 3. **The worktree IS the codebase.** Claude can read every file at `HEAD` directly — it doesn't need a separate scratch area.
18
+
19
+ Auth has no in-container scratch directory at all — the OAuth `setup-token` flow runs on the user's machine, not in tmux here. See `references/auth-flow.md`.
20
+
21
+ ## Spawning the session
22
+
23
+ The canonical spawn (per `SKILL.md`):
24
+
25
+ ```sh
26
+ tmux new-session -d -s cc-<task-id> -c /tmp/cc-<task-id> claude
27
+ ```
28
+
29
+ Flags worth knowing:
30
+
31
+ - `-d` — detached. The session runs in the background; your shell doesn't attach.
32
+ - `-s cc-<task-id>` — explicit session name. Required. Without `-s`, tmux picks `0`, `1`, … and a sibling delegation will clobber yours.
33
+ - `-c /tmp/cc-<task-id>` — start directory. Must be the worktree path. Claude Code reads `.claude/settings.json` relative to its cwd; if cwd is wrong, the Stop hook will not be registered.
34
+ - `claude` — the command. Just `claude`, not `claude -p`. The interactive TUI is the whole point.
35
+
36
+ Common mistake: forgetting `-c` and getting cwd `/agent` by default. The Stop hook won't fire because `/agent/.claude/settings.json` doesn't exist (or it does, and you've accidentally polluted someone else's hook config). Worse: claude in `/agent` operates on the live working tree instead of the worktree.
37
+
38
+ ## The init wait
39
+
40
+ `claude` prints a banner, performs auth verification, and renders its input box. This takes ~2–3 seconds on a warm cache, up to ~8s on cold start. You must wait for the input box to render before sending the first prompt, otherwise `send-keys` writes to a pane that isn't accepting input yet and your keystrokes are lost.
41
+
42
+ Two strategies:
43
+
44
+ 1. **Fixed sleep (simple, mostly works):** `sleep 3` after spawn, then `send-keys`. Robust against typical init times; occasionally lossy on cold start.
45
+ 2. **Poll for ready signal (robust):** every 500ms, `tmux capture-pane -t cc-<id> -p | tail -5` and look for an input-prompt marker. The exact marker varies by Claude Code version, but a unicode box-drawing character (`│`, `╭`, `╰`) at column 0 of the bottom rows is a reliable heuristic. Give up after 15 seconds and proceed anyway — late init is rare enough that the fixed-sleep fallback is fine.
46
+
47
+ The skill body uses the fixed sleep for simplicity. Upgrade to polling if you observe lost first prompts in practice.
48
+
49
+ ## Sending input
50
+
51
+ ```sh
52
+ tmux send-keys -t cc-<id> "<text>" Enter
53
+ ```
54
+
55
+ Notes:
56
+
57
+ - **Quote carefully.** The text is interpreted by tmux's send-keys before reaching the pty. Embedded `"` and `\` need escaping. For complex prompts, write the prompt to a file and use `tmux load-buffer + paste-buffer` instead of `send-keys`:
58
+
59
+ ```sh
60
+ echo "<prompt>" > prompt.txt
61
+ tmux load-buffer -t cc-<id> prompt.txt
62
+ tmux paste-buffer -t cc-<id>
63
+ tmux send-keys -t cc-<id> Enter
64
+ ```
65
+
66
+ - **`Enter` is the literal key name**, not the text "Enter". Other useful key names: `Escape`, `Tab`, `BSpace`, `Up`, `Down`, `C-c` (Ctrl+C), `C-d` (Ctrl+D for EOF).
67
+
68
+ - **Multi-line prompts**: send the body, then `Enter`. Claude Code's input box treats `Enter` as submit, so newlines in your text become submitted lines (not multi-line input). If you need a genuinely multi-line prompt, use the paste-buffer flow above with embedded newlines.
69
+
70
+ ## Polling for `.done`
71
+
72
+ The skill workflow polls the sentinel flag file, not the pane. This is the reliable path:
73
+
74
+ ```sh
75
+ budget=600 # 10 minutes in seconds
76
+ elapsed=0
77
+ while [ ! -f /tmp/cc-<id>/.done ]; do
78
+ if [ "$elapsed" -ge "$budget" ]; then
79
+ echo "Timeout reached"
80
+ break
81
+ fi
82
+ if ! tmux has-session -t cc-<id> 2>/dev/null; then
83
+ echo "tmux session died unexpectedly"
84
+ break
85
+ fi
86
+ sleep 0.5
87
+ elapsed=$((elapsed + 1))
88
+ done
89
+ ```
90
+
91
+ In your actual loop, translate to your tool calls: a check on `/tmp/cc-<id>/.done` existence, a check on `tmux has-session -t cc-<id>`, sleep, repeat. The shell snippet is illustrative.
92
+
93
+ ### Why 500ms cadence
94
+
95
+ - Faster polling (50–100ms) wastes CPU and is invisible to the user; the user's perception starts caring around 1s.
96
+ - Slower polling (2s+) adds visible latency to the multi-turn loop — every Stop adds up to 2s of pure wait.
97
+ - 500ms is the sweet spot: invisible latency, minimal CPU, plenty of headroom for short turns.
98
+
99
+ ### Session-died recovery
100
+
101
+ `tmux has-session` returns non-zero when the session is gone. Three reasons:
102
+
103
+ 1. **Claude crashed**: assertion failure, OOM, segfault. `capture-pane` before tmux GC'd the session would show a stack trace. After GC, the session is just gone; you can't recover the pane content.
104
+ 2. **Auth failed**: `claude` exited cleanly with "API key invalid" or similar. Pane would have shown the error briefly before the process exited.
105
+ 3. **User killed it externally**: someone ran `tmux kill-session -t cc-<id>` outside your control.
106
+
107
+ Recovery: surface to the user with whatever you have — `git diff main..cc-<id>` (which still works because the branch exists), `sentinel.json` from any prior turn, the JSONL if it exists. Then clean up: `git worktree remove --force /tmp/cc-<id>` + `git branch -D cc-<id>`. Ask whether to retry.
108
+
109
+ ## Capturing the pane (fallback path)
110
+
111
+ When the JSONL is missing or you need to see what the TUI showed:
112
+
113
+ ```sh
114
+ tmux capture-pane -t cc-<id> -p -S - -E -
115
+ ```
116
+
117
+ Flags:
118
+
119
+ - `-p` — print to stdout (default is to copy to a tmux buffer).
120
+ - `-S -` — start from the beginning of the scrollback.
121
+ - `-E -` — end at the current line.
122
+ - Without `-S -E`, you only capture the visible pane (rows × cols), losing everything that scrolled off.
123
+
124
+ The captured content includes ANSI escape codes by default. Pass `-e` to preserve them explicitly (rarely useful) or omit and strip via a regex after capture: `s/\x1b\[[0-9;]*[a-zA-Z]//g`.
125
+
126
+ ### ANSI gotchas
127
+
128
+ - **Progress spinners redraw the same line.** Capture-pane shows the final state of each cell, so spinners look harmless. But mid-capture, if claude is actively redrawing, you may catch a partial frame. Re-capture after `Stop` for a stable view.
129
+ - **Box-drawing characters** for the input box and message frames are Unicode (`╭ ╮ ╰ ╯ │ ─`). They render fine in modern terminals but mangle in some text post-processing. If you're showing pane content to the user via TypeClaw's TUI, just preserve as-is — Bun's stdout handles UTF-8.
130
+ - **Color codes**: standard 8-color (`\x1b[31m` etc.) and 256-color (`\x1b[38;5;Nm`). The strip regex above handles both.
131
+
132
+ ## Cleaning up
133
+
134
+ ```sh
135
+ tmux send-keys -t cc-<id> "/exit" Enter
136
+ sleep 1
137
+ tmux kill-session -t cc-<id> 2>/dev/null || true
138
+ git -C /agent worktree remove --force /tmp/cc-<id>
139
+ git -C /agent branch -D cc-<id>
140
+ ```
141
+
142
+ - `/exit` is Claude Code's built-in exit command. Cleaner than `C-c` — it lets the CLI flush the JSONL and close cleanly.
143
+ - `sleep 1` gives the CLI time to flush. Skip it and you may lose the last few JSONL lines.
144
+ - `kill-session ... || true` because the session may have already exited cleanly after `/exit`.
145
+ - `worktree remove --force` because the working tree has un-cherry-picked changes (claude's edits + the sentinel + the hook). `--force` is correct here because we're explicitly discarding.
146
+ - `branch -D` to delete the throwaway branch. `-D` (capital) because `cc-<id>` is unmerged into anything you care about.
147
+
148
+ ## Things you must not do in tmux driving
149
+
150
+ - **Do not omit `-s <name>` on `new-session`.** Anonymous sessions race across delegations.
151
+ - **Do not omit `-c /tmp/cc-<id>` on `new-session`.** Wrong cwd means wrong `.claude/settings.json`, and worse, claude operating on the live `/agent` working tree.
152
+ - **Do not skip the init wait.** Sending input before the TUI is ready loses the input silently.
153
+ - **Do not use `send-keys` with raw user-supplied strings without escaping.** Tmux's send-keys is mildly shell-like; embedded special chars get interpreted. Use `load-buffer + paste-buffer` for anything untrusted or complex.
154
+ - **Do not poll `capture-pane` as your primary done-signal.** Use the sentinel. `capture-pane` is for content retrieval, not lifecycle.
155
+ - **Do not kill the session with `C-c` if you can avoid it.** `/exit` is cleaner — it gives Claude Code time to flush the JSONL.
156
+ - **Do not assume `tmux has-session` returning success means claude is responsive.** A session can exist while claude is wedged. Pair with a wall-clock budget.
157
+ - **Do not `rm -rf /tmp/cc-<id>` instead of `git worktree remove`.** Pure rm leaves orphan refs in `/agent/.git/worktrees/cc-<id>/` and a dangling branch. The next delegation with the same id will fail with "branch already exists".
@@ -19,7 +19,7 @@ The runtime reads `typeclaw.json` at container startup. Some fields are picked u
19
19
  - `plugins` — array of plugin package names loaded at server boot. **Restart-required.**
20
20
  - `alias` — additional names the agent answers to when a channel message contains its name in plain text (no `<@id>` mention). The agent folder's directory name (`basename(agentDir)`) is always implicit; `alias` adds further forms (Latin transliteration, nicknames, Korean particles, etc.). Used by the channel engagement layer alongside the structural mention/reply/dm triggers. **Live-reloadable.**
21
21
  - `channels` — per-adapter engagement triggers and history-prefetch knobs for external messengers (Discord, Slack, Telegram, KakaoTalk). Access control lives in `roles`, not here. **Live-reloadable** — edits take effect on the next `reload` without a container restart.
22
- - `docker.file` — controls what ships in the autogenerated container image. Two layers: (1) **toggles** for opinionated apt packages (`tmux`, `gh`, `python`, `cjkFonts` default `true`; `ffmpeg` defaults `false`) — set the toggle to `false` to omit, or to a version string like `"2.40.0"` to apt-pin (`python` and `cjkFonts` are boolean-only). (2) **`append`** — extra Dockerfile lines spliced in right before `ENTRYPOINT` for anything the toggles don't cover. The whole Dockerfile is rewritten on every `start` from the typeclaw template. Lives under the `docker` namespace alongside future Docker-related blocks (e.g. `docker.compose`). **Restart-required** (next `typeclaw start` rebuilds the image).
22
+ - `docker.file` — controls what ships in the autogenerated container image. Two layers: (1) **toggles** for opinionated package installs (`tmux`, `gh`, `python`, `cjkFonts`, `cloudflared`, `xvfb` default `true`; `ffmpeg`, `claudeCode` default `false`) — set the toggle to `false` to omit, or to a version string like `"2.40.0"` to apt-pin (`python`, `cjkFonts`, `cloudflared`, `xvfb`, and `claudeCode` are boolean-only). Most toggles install apt packages with BuildKit cache mounts; `cloudflared` and `claudeCode` are exceptions — `cloudflared` downloads the pinned GitHub release, `claudeCode` runs Anthropic's official `curl | bash` installer. (2) **`append`** — extra Dockerfile lines spliced in right before `ENTRYPOINT` for anything the toggles don't cover. The whole Dockerfile is rewritten on every `start` from the typeclaw template. Lives under the `docker` namespace alongside future Docker-related blocks (e.g. `docker.compose`). **Restart-required** (next `typeclaw start` rebuilds the image).
23
23
  - `git.ignore.append` — extra `.gitignore` patterns `typeclaw start` splices into the TypeClaw-owned `.gitignore` before the protected TypeClaw rules. The whole `.gitignore` is rewritten and auto-committed on every `start` when it changes; `append` is the supported escape hatch for local ignore patterns without editing the managed file by hand. Lives under the `git` namespace. **Restart-required** (next `typeclaw start` refreshes and commits `.gitignore`).
24
24
  - `portForward` — allow/deny policy for the auto port-forwarder (the host-stage `_hostd` daemon's portbroker). When the agent runs a server inside the container that LISTENs on a TCP port, the broker proxies it to the same port number on `127.0.0.1` of the host so the user can hit it directly. `portForward` decides which ports are allowed through. **Restart-required** — the broker captures the policy at register time on `typeclaw start`.
25
25
 
@@ -39,18 +39,18 @@ You yourself cannot run `typeclaw restart` — that is a host-stage command and
39
39
 
40
40
  `typeclaw.json` is a single JSON object with these fields:
41
41
 
42
- | Field | Required | Type | Notes |
43
- | ------------- | -------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
44
- | `$schema` | no | string | Path to `typeclaw.schema.json` for editor autocompletion. Scaffolded as `./node_modules/typeclaw/typeclaw.schema.json`. Leave it alone unless the user moves it. |
45
- | `port` | no | integer | 1–65535. Defaults to `8973` (T9 spelling of "TYPE"). Change only if the default collides with something on the user's host. **Restart-required.** |
46
- | `model` | no | string | Must be one of the values listed in the **Allowed models** section below. Defaults to `openai/gpt-5.4-nano`. **Live-reloadable.** |
47
- | `mounts` | no | array of objects | Host directories bind-mounted into your container. Defaults to `[]` (no host paths exposed). Omitted from scaffolded `typeclaw.json` — add it only when the user wants host paths exposed. See **Mounts** section below. **Restart-required.** |
48
- | `plugins` | no | array of strings | Plugin package names loaded at server boot. Defaults to `[]`. **Restart-required.** Plugin-owned config blocks live alongside as additional top-level keys; see **Plugin config blocks**. |
49
- | `alias` | no | array of strings | Additional names the agent answers to in channel engagement, on top of the implicit `basename(agentDir)`. Each entry is a non-empty trimmed string matched case-insensitively as a substring of the inbound text. Defaults to `[]`. Hatching populates this with the agent's chosen name. See **Alias** section below. **Live-reloadable.** |
50
- | `channels` | no | object | Per-adapter engagement triggers and history-prefetch knobs for external messengers. Defaults to `{}` (no adapters configured). `typeclaw init` scaffolds an empty block per requested adapter (e.g. `"discord-bot": {}`) and the schema fills in defaults. Channel access control lives in `roles` — see the `typeclaw-permissions` skill. **Live-reloadable.** See **Channels** section below. |
51
- | `portForward` | no | object | Allow/deny policy for the host-stage portbroker that auto-forwards container LISTEN ports to `127.0.0.1` on the host. Defaults to `{ "allow": "*" }` (forward everything). Omitted from scaffolded `typeclaw.json`. **Restart-required.** See **portForward** section below. |
52
- | `docker` | no | object | Namespace for Docker-related blocks. Today the only child is `docker.file` — toggles (`tmux`, `gh`, `python`, `ffmpeg`, `cjkFonts`) gate opinionated apt packages; `append` adds custom Dockerfile lines just before `ENTRYPOINT`. `docker.file` defaults to `{ ffmpeg: false, gh: true, python: true, tmux: true, cjkFonts: true, append: [] }`. Omitted from scaffolded `typeclaw.json`. **Restart-required** (next `typeclaw start` rebuilds the image). See **Dockerfile** section below. |
53
- | `git` | no | object | Namespace for git-related blocks. Today the only child is `git.ignore` — extra patterns spliced into the autogenerated `.gitignore` before TypeClaw's protected rules. `git.ignore` defaults to `{ "append": [] }`. Omitted from scaffolded `typeclaw.json`. **Restart-required** (next `typeclaw start` refreshes `.gitignore`). See **Gitignore** section below. |
42
+ | Field | Required | Type | Notes |
43
+ | ------------- | -------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
44
+ | `$schema` | no | string | Path to `typeclaw.schema.json` for editor autocompletion. Scaffolded as `./node_modules/typeclaw/typeclaw.schema.json`. Leave it alone unless the user moves it. |
45
+ | `port` | no | integer | 1–65535. Defaults to `8973` (T9 spelling of "TYPE"). Change only if the default collides with something on the user's host. **Restart-required.** |
46
+ | `model` | no | string | Must be one of the values listed in the **Allowed models** section below. Defaults to `openai/gpt-5.4-nano`. **Live-reloadable.** |
47
+ | `mounts` | no | array of objects | Host directories bind-mounted into your container. Defaults to `[]` (no host paths exposed). Omitted from scaffolded `typeclaw.json` — add it only when the user wants host paths exposed. See **Mounts** section below. **Restart-required.** |
48
+ | `plugins` | no | array of strings | Plugin package names loaded at server boot. Defaults to `[]`. **Restart-required.** Plugin-owned config blocks live alongside as additional top-level keys; see **Plugin config blocks**. |
49
+ | `alias` | no | array of strings | Additional names the agent answers to in channel engagement, on top of the implicit `basename(agentDir)`. Each entry is a non-empty trimmed string matched case-insensitively as a substring of the inbound text. Defaults to `[]`. Hatching populates this with the agent's chosen name. See **Alias** section below. **Live-reloadable.** |
50
+ | `channels` | no | object | Per-adapter engagement triggers and history-prefetch knobs for external messengers. Defaults to `{}` (no adapters configured). `typeclaw init` scaffolds an empty block per requested adapter (e.g. `"discord-bot": {}`) and the schema fills in defaults. Channel access control lives in `roles` — see the `typeclaw-permissions` skill. **Live-reloadable.** See **Channels** section below. |
51
+ | `portForward` | no | object | Allow/deny policy for the host-stage portbroker that auto-forwards container LISTEN ports to `127.0.0.1` on the host. Defaults to `{ "allow": "*" }` (forward everything). Omitted from scaffolded `typeclaw.json`. **Restart-required.** See **portForward** section below. |
52
+ | `docker` | no | object | Namespace for Docker-related blocks. Today the only child is `docker.file` — toggles (`tmux`, `gh`, `python`, `ffmpeg`, `cjkFonts`, `cloudflared`, `claudeCode`) gate opinionated package installs; `append` adds custom Dockerfile lines just before `ENTRYPOINT`. `docker.file` defaults to `{ ffmpeg: false, gh: true, python: true, tmux: true, cjkFonts: true, cloudflared: true, claudeCode: false, append: [] }`. Omitted from scaffolded `typeclaw.json`. **Restart-required** (next `typeclaw start` rebuilds the image). See **Dockerfile** section below. |
53
+ | `git` | no | object | Namespace for git-related blocks. Today the only child is `git.ignore` — extra patterns spliced into the autogenerated `.gitignore` before TypeClaw's protected rules. `git.ignore` defaults to `{ "append": [] }`. Omitted from scaffolded `typeclaw.json`. **Restart-required** (next `typeclaw start` refreshes `.gitignore`). See **Gitignore** section below. |
54
54
 
55
55
  > **Top-level keys not in this table are not "ignored unknowns" anymore** — they are reserved for **plugin config blocks**. The schema's `catchall(z.unknown())` preserves them, and the plugin loader hands each block to its owning plugin's `configSchema` for validation. The bundled memory plugin owns `memory` at the top level — see the `typeclaw-memory` skill for that block's semantics. Do not write a top-level key unless you know which plugin owns it.
56
56
 
@@ -65,7 +65,7 @@ A scaffolded `typeclaw.json` looks like:
65
65
  }
66
66
  ```
67
67
 
68
- The runtime fills in defaults for any omitted field: `port` → `8973`, `mounts` → `[]` (no host paths exposed), `plugins` → `[]`, `channels` → `{}` (no adapters configured), `portForward` → `{ "allow": "*" }` (forward every container LISTEN port), `docker` → `{ "file": { "ffmpeg": false, "gh": true, "python": true, "tmux": true, "cjkFonts": true, "append": [] } }` (tmux/gh/python/cjkFonts pre-installed, ffmpeg off, no custom build steps), `git` → `{ "ignore": { "append": [] } }` (no custom ignore patterns). `typeclaw init` deliberately omits any field whose default is owned elsewhere — `mounts`, `portForward`, `docker`, and `git` default via `configSchema`, and the bundled memory plugin owns its own `memory` defaults — so the scaffolded file stays minimal and the user sees only fields they actually need to think about. Add a `memory` block (a **plugin config block** owned by the bundled memory plugin) only when overriding its defaults; see the `typeclaw-memory` skill for the schema.
68
+ The runtime fills in defaults for any omitted field: `port` → `8973`, `mounts` → `[]` (no host paths exposed), `plugins` → `[]`, `channels` → `{}` (no adapters configured), `portForward` → `{ "allow": "*" }` (forward every container LISTEN port), `docker` → `{ "file": { "ffmpeg": false, "gh": true, "python": true, "tmux": true, "cjkFonts": true, "cloudflared": true, "claudeCode": false, "append": [] } }` (tmux/gh/python/cjkFonts/cloudflared pre-installed; ffmpeg and claudeCode off; no custom build steps), `git` → `{ "ignore": { "append": [] } }` (no custom ignore patterns). `typeclaw init` deliberately omits any field whose default is owned elsewhere — `mounts`, `portForward`, `docker`, and `git` default via `configSchema`, and the bundled memory plugin owns its own `memory` defaults — so the scaffolded file stays minimal and the user sees only fields they actually need to think about. Add a `memory` block (a **plugin config block** owned by the bundled memory plugin) only when overriding its defaults; see the `typeclaw-memory` skill for the schema.
69
69
 
70
70
  If the user said yes to "Wire a Discord bot?" during `typeclaw init`, the scaffold also includes:
71
71
 
@@ -337,19 +337,22 @@ Off switch — the broker is constructed but never opens a WS, no LISTEN gets fo
337
337
 
338
338
  The `docker.file` block has two layers of customization:
339
339
 
340
- 1. **Toggles** for opinionated apt packages typeclaw knows how to install with proper layer caching (`tmux`, `gh`, `python`, `ffmpeg`, `cjkFonts`). Boolean for on/off, version string for an apt pin (e.g. `"gh": "2.40.0"` → `gh=2.40.0`). Use these whenever they cover what the user wants they get BuildKit cache-mount benefits and, for `gh`, automatic keyring layer gating.
340
+ 1. **Toggles** for opinionated package installs typeclaw knows how to layer correctly (`tmux`, `gh`, `python`, `ffmpeg`, `cjkFonts`, `cloudflared`, `claudeCode`). Most are apt packages — boolean for on/off, version string for an apt pin (e.g. `"gh": "2.40.0"` → `gh=2.40.0`) — and benefit from BuildKit cache mounts. `cloudflared` and `claudeCode` are the exceptions: `cloudflared` downloads the pinned GitHub release, `claudeCode` runs Anthropic's `curl | bash` installer; both are boolean-only. Use a toggle whenever it covers what the user wants over a hand-rolled `append` entry.
341
341
  2. **`append`** is the escape hatch for everything the toggles don't cover. An array of single-line Dockerfile instructions spliced in right before `ENTRYPOINT`, prefixed with a `# Custom lines from typeclaw.json#docker.file.append.` comment.
342
342
 
343
343
  ### Fields
344
344
 
345
- | Field | Required | Type | Notes |
346
- | ---------- | -------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
347
- | `tmux` | no | boolean \| string | Default `true`. `false` omits tmux from the apt install. String pins the Debian package version (e.g. `"3.3a-3"` → `tmux=3.3a-3`). |
348
- | `gh` | no | boolean \| string | Default `true`. `false` omits **both** the `gh` package and the GitHub CLI keyring bootstrap layer (skipping the network roundtrip on cold builds). String pins the version. |
349
- | `python` | no | boolean | Default `true`. Fans out to `python3 python3-pip python3-venv python-is-python3` (the bundle that makes `python` and `pip` resolve correctly inside the container). Boolean-only — no version pin, because Debian's `python3` is a meta-package that doesn't accept a useful pin. |
350
- | `ffmpeg` | no | boolean \| string | Default `false`. `true` apt-installs ffmpeg (~80 MB of codecs). String pins the version. |
351
- | `cjkFonts` | no | boolean | Default `true`. Installs `fonts-noto-cjk` (~56 MB) so Chromium (used by `agent-browser`) renders Korean/Japanese/Chinese glyphs correctly in screenshots, `page.pdf()`, and other raster output. `false` skips the layer entirely (DOM/innerText scraping is unaffected by font absence — only raster output shows tofu boxes). Boolean-only: the package is a metapackage tracking upstream Noto, no useful apt pin. |
352
- | `append` | no | array of strings | Each entry is a single Dockerfile line schema **rejects** entries containing `\n` or `\r`. Defaults to `[]`. Splice happens just before `ENTRYPOINT`, after `ENV NODE_ENV=production`. |
345
+ | Field | Required | Type | Notes |
346
+ | ------------- | -------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
347
+ | `tmux` | no | boolean \| string | Default `true`. `false` omits tmux from the apt install. String pins the Debian package version (e.g. `"3.3a-3"` → `tmux=3.3a-3`). |
348
+ | `gh` | no | boolean \| string | Default `true`. `false` omits **both** the `gh` package and the GitHub CLI keyring bootstrap layer (skipping the network roundtrip on cold builds). String pins the version. |
349
+ | `python` | no | boolean | Default `true`. Fans out to `python3 python3-pip python3-venv python-is-python3` (the bundle that makes `python` and `pip` resolve correctly inside the container). Boolean-only — no version pin, because Debian's `python3` is a meta-package that doesn't accept a useful pin. |
350
+ | `ffmpeg` | no | boolean \| string | Default `false`. `true` apt-installs ffmpeg (~80 MB of codecs). String pins the version. |
351
+ | `cjkFonts` | no | boolean | Default `true`. Installs `fonts-noto-cjk` (~56 MB) so Chromium (used by `agent-browser`) renders Korean/Japanese/Chinese glyphs correctly in screenshots, `page.pdf()`, and other raster output. `false` skips the layer entirely (DOM/innerText scraping is unaffected by font absence — only raster output shows tofu boxes). Boolean-only: the package is a metapackage tracking upstream Noto, no useful apt pin. |
352
+ | `cloudflared` | no | boolean | Default `true`. Downloads the pinned `cloudflared` GitHub release (~35 MB) into the image so `cloudflare-quick` tunnels work on the next `start` without a separate Dockerfile edit. `false` skips the layer entirely on agents that don't use tunnels. Boolean-only — pinning is owned by the typeclaw release. |
353
+ | `xvfb` | no | boolean | Default `true`. Installs `xvfb` (~5 MB) so the entrypoint shim can spawn a virtual X server and export `DISPLAY=:99`, giving headed Chrome (agent-browser `--headed`, headful Playwright) a real X11 display to defeat headless-mode WAF fingerprinting. `false` skips the layer; the shim self-heals (no `Xvfb` on PATH → execs the agent without `DISPLAY`). Boolean-only — xvfb tracks the upstream X server release with no useful apt pin. |
354
+ | `claudeCode` | no | boolean | Default `false`. `true` runs Anthropic's official `curl -fsSL https://claude.ai/install.sh \| bash` in a dedicated layer (between agent-browser and the entrypoint shim). Not apt: no version-pin variant; the upstream installer manages channels via env vars. Pairs with the `typeclaw-claude-code` skill, which documents the auth + tmux-driven usage flow. |
355
+ | `append` | no | array of strings | Each entry is a single Dockerfile line — schema **rejects** entries containing `\n` or `\r`. Defaults to `[]`. Splice happens just before `ENTRYPOINT`, after `ENV NODE_ENV=production`. |
353
356
 
354
357
  Toggle version strings reject whitespace and `=` (apt-injection guard) — pass just the version, not `pkg=ver`.
355
358
 
@@ -397,7 +400,7 @@ The toggle-driven apt install benefits from BuildKit `--mount=type=cache` on `/v
397
400
  ### When the user asks "install <package> in the container" / "add a Dockerfile line"
398
401
 
399
402
  1. **Read `typeclaw.json`.**
400
- 2. **Check if a toggle covers it.** If the package is `tmux`, `gh`, `python`, `ffmpeg`, or `cjkFonts` (CJK glyph rendering for `agent-browser` screenshots), prefer the toggle: `"docker": { "file": { "ffmpeg": true } }`. For a pinned version, pass the version string: `"gh": "2.40.0"`. This is faster (BuildKit cache mount) and clearer than `append`.
403
+ 2. **Check if a toggle covers it.** If the package is `tmux`, `gh`, `python`, `ffmpeg`, `cjkFonts` (CJK glyph rendering for `agent-browser` screenshots), `cloudflared` (Cloudflare Quick tunnels), or Anthropic's Claude Code CLI (`claudeCode`), prefer the toggle: `"docker": { "file": { "ffmpeg": true } }`. For a pinned version of an apt toggle, pass the version string: `"gh": "2.40.0"`. This is faster (BuildKit cache mount) and clearer than `append`. `cjkFonts`, `cloudflared`, and `claudeCode` are boolean-only — no version-pin variant.
401
404
  3. **Otherwise, use `append`.** Decide on a single-line entry — for apt installs, prefer one `RUN apt-get update && apt-get install -y --no-install-recommends <pkg> && rm -rf /var/lib/apt/lists/*` line. For env vars, one `ENV` line per variable.
402
405
  4. **Validate no embedded newlines** (`append` only). Multi-step logic must be `&&`-chained on one line, not split across array entries unless those entries are independent Dockerfile instructions.
403
406
  5. **Append to `docker.file.append`** (creating the field if it doesn't exist). Preserve existing entries.
@@ -618,7 +621,7 @@ Never echo, log, or commit values from `.env` or `secrets.json`. Both are gitign
618
621
  - `channels.<adapter>.allow` (legacy) is silently dropped on parse; `migrateLegacyConfigShape` lifts it into `roles.member.match` on load. See the `typeclaw-permissions` skill.
619
622
  - If `portForward` is set: `allow` is either `"*"` or an array of integers (1–65535); `deny`, if present, is an array of integers and **only valid when `allow` is `"*"`** (the schema rejects `deny` paired with a number-array `allow`)
620
623
  - If `docker.file.append` is set: array of strings, each with no embedded `\n` or `\r` (multi-step shell logic goes in a single `&&`-chained `RUN` entry)
621
- - If any `docker.file` toggle is set: `tmux`/`gh`/`ffmpeg` are boolean or version string (no whitespace, no `=`); `python` and `cjkFonts` are boolean only
624
+ - If any `docker.file` toggle is set: `tmux`/`gh`/`ffmpeg` are boolean or version string (no whitespace, no `=`); `python`, `cjkFonts`, `cloudflared`, and `claudeCode` are boolean only
622
625
  - No unknown top-level keys you invented — keys outside the well-known ten are interpreted as **plugin config blocks** and only do something if a plugin owns them. Inventing one means the user thinks it took effect and it did not.
623
626
 
624
627
  ## Things you must not do
@@ -637,7 +640,7 @@ Never echo, log, or commit values from `.env` or `secrets.json`. Both are gitign
637
640
  - **Do not promise to post to a channel the speaker's role does not cover.** The router drops every inbound where the speaking author resolves to a role without `channel.respond`. If the user wants you to post somewhere new, the prerequisite is a `roles` edit + restart, not a retry.
638
641
  - **Do not conflate "stop replying" with "remove the role's match-rule".** Removing the match-rule cuts off both inbound visibility and outbound posting. If the user just wants quieter behavior, edit `engagement` instead.
639
642
  - **Do not edit the `Dockerfile` directly.** It is autogenerated and rewritten on every `typeclaw start` from `src/init/dockerfile.ts` in the typeclaw repo. Manual edits will be silently overwritten (and auto-committed away if the working tree is dirty). Customizations belong in the `docker.file` block (toggles or `append`).
640
- - **Do not reach for `docker.file.append` when a toggle covers it.** If the user wants tmux, gh, python, ffmpeg, or fonts-noto-cjk (cjkFonts) installed (or removed, or pinned), use the toggle it's the cache-mounted path. `append` for these is slower and harder to read.
643
+ - **Do not reach for `docker.file.append` when a toggle covers it.** If the user wants tmux, gh, python, ffmpeg, fonts-noto-cjk (cjkFonts), cloudflared, or Anthropic's Claude Code CLI (claudeCode) installed (or removed, or pinned), use the toggle. The apt toggles are the cache-mounted path; `cloudflared` and `claudeCode` are non-apt boolean toggles, and the `claudeCode` toggle pairs with the `typeclaw-claude-code` skill that documents auth + usage. `append` for any of these is slower and harder to read.
641
644
  - **Do not use `docker.file.append` for things that belong in the template.** If the user wants a system package _every_ typeclaw user should have, that's a typeclaw release, not a per-agent `append`. Suggest filing an issue.
642
645
  - **Do not put multiline strings in `docker.file.append`.** The schema rejects entries with embedded `\n`/`\r`. Use one entry per Dockerfile instruction; chain shell logic with `&&` on one line.
643
646
  - **Do not pass `pkg=ver` as a toggle version string.** The schema rejects `=` in version strings. Pass just the version (`"gh": "2.40.0"`); the renderer prepends `pkg=` itself. Same for whitespace — version strings cannot contain spaces.
@@ -925,6 +925,7 @@
925
925
  "cjkFonts": true,
926
926
  "cloudflared": true,
927
927
  "xvfb": true,
928
+ "claudeCode": false,
928
929
  "append": []
929
930
  }
930
931
  },
@@ -939,6 +940,7 @@
939
940
  "cjkFonts": true,
940
941
  "cloudflared": true,
941
942
  "xvfb": true,
943
+ "claudeCode": false,
942
944
  "append": []
943
945
  },
944
946
  "type": "object",
@@ -995,6 +997,10 @@
995
997
  "default": true,
996
998
  "type": "boolean"
997
999
  },
1000
+ "claudeCode": {
1001
+ "default": false,
1002
+ "type": "boolean"
1003
+ },
998
1004
  "append": {
999
1005
  "default": [],
1000
1006
  "type": "array",