typeclaw 0.5.1 → 0.7.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 +34 -84
  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 +42 -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/init.ts +8 -1
  26. package/src/cli/oauth-callbacks.ts +64 -34
  27. package/src/cli/provider.ts +9 -4
  28. package/src/config/config.ts +73 -16
  29. package/src/config/index.ts +3 -0
  30. package/src/config/providers.ts +106 -0
  31. package/src/cron/index.ts +3 -0
  32. package/src/cron/schema.ts +20 -0
  33. package/src/init/dockerfile.ts +44 -5
  34. package/src/init/models-dev.ts +1 -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 +12 -0
@@ -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.
@@ -26,6 +26,9 @@
26
26
  "openai/gpt-5.4-mini",
27
27
  "openai/gpt-5.4",
28
28
  "openai/gpt-5.5",
29
+ "anthropic/claude-haiku-4-5",
30
+ "anthropic/claude-sonnet-4-6",
31
+ "anthropic/claude-opus-4-7",
29
32
  "openai-codex/gpt-5.4-mini",
30
33
  "openai-codex/gpt-5.4",
31
34
  "openai-codex/gpt-5.5",
@@ -50,6 +53,9 @@
50
53
  "openai/gpt-5.4-mini",
51
54
  "openai/gpt-5.4",
52
55
  "openai/gpt-5.5",
56
+ "anthropic/claude-haiku-4-5",
57
+ "anthropic/claude-sonnet-4-6",
58
+ "anthropic/claude-opus-4-7",
53
59
  "openai-codex/gpt-5.4-mini",
54
60
  "openai-codex/gpt-5.4",
55
61
  "openai-codex/gpt-5.5",
@@ -925,6 +931,7 @@
925
931
  "cjkFonts": true,
926
932
  "cloudflared": true,
927
933
  "xvfb": true,
934
+ "claudeCode": false,
928
935
  "append": []
929
936
  }
930
937
  },
@@ -939,6 +946,7 @@
939
946
  "cjkFonts": true,
940
947
  "cloudflared": true,
941
948
  "xvfb": true,
949
+ "claudeCode": false,
942
950
  "append": []
943
951
  },
944
952
  "type": "object",
@@ -995,6 +1003,10 @@
995
1003
  "default": true,
996
1004
  "type": "boolean"
997
1005
  },
1006
+ "claudeCode": {
1007
+ "default": false,
1008
+ "type": "boolean"
1009
+ },
998
1010
  "append": {
999
1011
  "default": [],
1000
1012
  "type": "array",