typeclaw 0.5.1 → 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.
- package/README.md +4 -0
- package/package.json +1 -1
- package/src/agent/index.ts +80 -8
- package/src/agent/live-subagents.ts +215 -0
- package/src/agent/plugin-tools.ts +60 -20
- package/src/agent/session-origin.ts +15 -0
- package/src/agent/subagents.ts +140 -3
- package/src/agent/system-prompt.ts +40 -0
- package/src/agent/tools/channel-reply.ts +24 -1
- package/src/agent/tools/channel-send.ts +26 -1
- package/src/agent/tools/spawn-subagent.ts +283 -0
- package/src/agent/tools/subagent-cancel.ts +96 -0
- package/src/agent/tools/subagent-output.ts +192 -0
- package/src/bundled-plugins/agent-browser/skills/agent-browser/SKILL.md +26 -0
- package/src/bundled-plugins/explorer/explorer.ts +103 -0
- package/src/bundled-plugins/explorer/index.ts +11 -0
- package/src/bundled-plugins/guard/index.ts +12 -1
- package/src/bundled-plugins/guard/policies/managed-config.ts +139 -0
- package/src/bundled-plugins/guard/policy.ts +1 -0
- package/src/bundled-plugins/operator/index.ts +11 -0
- package/src/bundled-plugins/operator/operator.ts +76 -0
- package/src/bundled-plugins/scout/index.ts +11 -0
- package/src/bundled-plugins/scout/scout.ts +94 -0
- package/src/channels/router.ts +32 -0
- package/src/config/config.ts +45 -12
- package/src/config/index.ts +3 -0
- package/src/cron/index.ts +3 -0
- package/src/cron/schema.ts +20 -0
- package/src/init/dockerfile.ts +44 -5
- package/src/permissions/builtins.ts +23 -2
- package/src/plugin/define.ts +2 -0
- package/src/plugin/index.ts +2 -0
- package/src/plugin/types.ts +15 -22
- package/src/run/bundled-plugins.ts +6 -0
- package/src/run/channel-session-factory.ts +19 -0
- package/src/run/index.ts +56 -6
- package/src/server/index.ts +103 -0
- package/src/skills/typeclaw-claude-code/SKILL.md +273 -0
- package/src/skills/typeclaw-claude-code/references/auth-flow.md +135 -0
- package/src/skills/typeclaw-claude-code/references/stop-hook.md +99 -0
- package/src/skills/typeclaw-claude-code/references/tmux-driving.md +157 -0
- package/src/skills/typeclaw-config/SKILL.md +29 -26
- package/typeclaw.schema.json +6 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Auth flow — interactive
|
|
2
|
+
|
|
3
|
+
Deep dive for the auth paths. Read it when `SKILL.md`'s "First-time auth (interactive)" section sends you here, or when an auth attempt fails and you need to understand what went wrong.
|
|
4
|
+
|
|
5
|
+
The two paths are intentionally symmetric: in both, the user produces one string on their side, pastes it to you, you validate it, you do read-modify-write on `.env`, you offer a restart. Only the credential differs.
|
|
6
|
+
|
|
7
|
+
## Path A — API key (recap)
|
|
8
|
+
|
|
9
|
+
The API key path lives entirely in `SKILL.md` because there's nothing to elaborate. Summary:
|
|
10
|
+
|
|
11
|
+
1. Prompt user for `sk-ant-…`.
|
|
12
|
+
2. Validate `/^sk-ant-[A-Za-z0-9_-]{20,}$/`.
|
|
13
|
+
3. Read `.env`, merge `ANTHROPIC_API_KEY=<value>` into the parsed map, reconstruct full content, write with `acknowledgeGuards: { nonWorkspaceWrite: true }`.
|
|
14
|
+
4. Verify.
|
|
15
|
+
5. Ask before restart.
|
|
16
|
+
|
|
17
|
+
When to recommend it: the user has an **Anthropic Console** workspace (API billing, no Claude subscription). They get their key from `console.anthropic.com`. Cost is metered per-token against the Console workspace.
|
|
18
|
+
|
|
19
|
+
## Path B — OAuth long-lived token, generated on the user's machine
|
|
20
|
+
|
|
21
|
+
This is the path for users with a Claude **Pro / Max / Team / Enterprise** subscription. Inference cost is bounded by the subscription's monthly Agent SDK credit pool, not per-token.
|
|
22
|
+
|
|
23
|
+
The token is generated by `claude setup-token`, which is Anthropic's own one-time setup command. From the [official docs](https://code.claude.com/docs/en/authentication):
|
|
24
|
+
|
|
25
|
+
> For CI pipelines, scripts, or other environments where interactive browser login isn't available, generate a one-year OAuth token with `claude setup-token`. The command walks you through OAuth authorization and prints a token to the terminal. It does not save the token anywhere; copy it and set it as the `CLAUDE_CODE_OAUTH_TOKEN` environment variable wherever you want to authenticate.
|
|
26
|
+
|
|
27
|
+
A typeclaw container is precisely that environment ("CI pipelines, scripts, or other environments where interactive browser login isn't available"). The user runs `setup-token` on their own machine — where they already have `claude` installed and `/login`-ed — copies the printed token, and pastes it to you.
|
|
28
|
+
|
|
29
|
+
### Why on the user's machine, not in the container
|
|
30
|
+
|
|
31
|
+
This was originally implemented as an in-container tmux dance: agent spawns `claude setup-token` in a tmux pane, scrapes the URL with `capture-pane`, surfaces it to the user, brokers the auth code back, regex-extracts the token from the pane. It worked, barely. It cost ~150 lines of pane-capture mechanics, ANSI stripping, URL-or-code parsing, retry logic, and timing assumptions that broke on every Claude Code version bump.
|
|
32
|
+
|
|
33
|
+
The user-machine flow is strictly better:
|
|
34
|
+
|
|
35
|
+
1. **Zero in-container surface area.** No tmux session, no pane capture, no version-locked regex matching the prompt wording, no 30-second polling budget, no race between OAuth-code single-use and your retry loop.
|
|
36
|
+
2. **The user already has `claude` installed locally.** They had to, to have a subscription worth using `setup-token` against. The marginal install cost is zero.
|
|
37
|
+
3. **The browser is already on the user's machine.** No matter where the container lives — laptop, remote VM, shared workstation, cloud sandbox — the user's browser is where the user is. `setup-token` on the user's machine has a working `localhost:1455` callback by definition; no cross-device dance needed.
|
|
38
|
+
4. **Failure modes are easier to debug.** When `setup-token` fails on the user's machine, the user sees the error directly. When it failed in the container, you had to surface a stripped-ANSI capture-pane snapshot and hope the user could decipher it.
|
|
39
|
+
5. **The token has no network dependency from inside the container.** Once it's in `.env`, `claude` reads it from the environment on startup — no token-refresh round-trips, no `api.anthropic.com` reachability requirement at auth time (only at inference time, which the agent needs anyway).
|
|
40
|
+
|
|
41
|
+
There is no remaining case where running `setup-token` inside the container is preferable. The only thing the container needs is the resulting token string.
|
|
42
|
+
|
|
43
|
+
### Step-by-step
|
|
44
|
+
|
|
45
|
+
1. **Confirm prerequisites with the user, in one message:**
|
|
46
|
+
|
|
47
|
+
> To set up OAuth auth, you'll generate a long-lived token on your own machine. Two prerequisites:
|
|
48
|
+
>
|
|
49
|
+
> 1. Do you have the `claude` CLI installed locally? If not: install from `claude.com/code`, then `claude login` with your Claude Pro / Max / Team / Enterprise account.
|
|
50
|
+
> 2. Do you have a paid Claude subscription? (`setup-token` requires Pro, Max, Team, or Enterprise — it doesn't work on free accounts.)
|
|
51
|
+
>
|
|
52
|
+
> Once both are true, reply "ready" and I'll send the next step.
|
|
53
|
+
|
|
54
|
+
This single confirmation up-front is the difference between a one-paste flow and a multi-turn debugging session when the user discovers mid-flow that their CLI isn't installed.
|
|
55
|
+
|
|
56
|
+
2. **When the user confirms, send the generation instructions:**
|
|
57
|
+
|
|
58
|
+
> Great. On your machine, run:
|
|
59
|
+
>
|
|
60
|
+
> ```sh
|
|
61
|
+
> claude setup-token
|
|
62
|
+
> ```
|
|
63
|
+
>
|
|
64
|
+
> It opens a browser, you authorize with your Claude account, and then prints **one long token** on the terminal. It looks something like:
|
|
65
|
+
>
|
|
66
|
+
> ```
|
|
67
|
+
> sk-ant-oat01-<long random string>
|
|
68
|
+
> ```
|
|
69
|
+
>
|
|
70
|
+
> Copy the whole token (just the token, not any surrounding text) and paste it back to me. The token is valid for one year and authenticates against your Claude subscription — treat it like a password.
|
|
71
|
+
|
|
72
|
+
3. **Wait for the user's reply.** Expected shapes:
|
|
73
|
+
- **A bare token string.** Typically starts with `sk-ant-oat01-` but Anthropic has changed the prefix before and may again — do not hardcode the prefix.
|
|
74
|
+
- **The full line including `CLAUDE_CODE_OAUTH_TOKEN=`** if they pasted the `export` line they wrote themselves. Strip the `CLAUDE_CODE_OAUTH_TOKEN=` prefix (and any leading `export `) before validating.
|
|
75
|
+
- **An error message** if `setup-token` failed on their side. See the failure-mode list below.
|
|
76
|
+
- **"cancel"** or equivalent. Drop the flow cleanly.
|
|
77
|
+
|
|
78
|
+
4. **Parse and validate**, in order:
|
|
79
|
+
1. Trim leading/trailing whitespace.
|
|
80
|
+
2. If the string starts with `export ` (with the space), drop the `export ` prefix.
|
|
81
|
+
3. If the string starts with `CLAUDE_CODE_OAUTH_TOKEN=`, drop that prefix. Also strip surrounding single or double quotes that the user's shell prompt may have included.
|
|
82
|
+
4. Validate the remainder: `/^[A-Za-z0-9_-]{30,}$/`. Tokens are opaque alphanumeric blobs with `_` and `-` only (current observed prefix is `sk-ant-oat01-` but Anthropic has changed prefixes before — validate by shape, not prefix). If the token format ever grows to include `.`, `/`, or other characters, this regex will reject valid tokens; widen the character class then, not preemptively.
|
|
83
|
+
5. If validation fails, ask once more: "That doesn't look like a `claude setup-token` token — it should be one long string with no spaces or newlines. Paste just the token. Or say 'cancel' to switch to API-key auth instead."
|
|
84
|
+
6. If the second attempt also fails, drop OAuth and recommend the API-key path.
|
|
85
|
+
|
|
86
|
+
5. **Confirm receipt without echoing the token.** Reply something like "Got it, validating and writing to `.env`." Never include the token in your reply, in a log line, in a sentinel, in a commit message, or anywhere else.
|
|
87
|
+
|
|
88
|
+
6. **Read `.env`** (existing content, may not exist).
|
|
89
|
+
|
|
90
|
+
7. **Parse into a key→value map.** Be tolerant of comments (`#`-prefixed lines), blank lines, and quoted values. Preserve order and comments when reconstructing.
|
|
91
|
+
|
|
92
|
+
8. **Merge `CLAUDE_CODE_OAUTH_TOKEN=<value>`.** Add if absent, replace if present. Do not quote — Docker's `--env-file` parser is brittle around quotes, and the token has no whitespace by validation.
|
|
93
|
+
|
|
94
|
+
9. **Write back with `acknowledgeGuards: { nonWorkspaceWrite: true }`.** `.env` is in the `nonWorkspaceWrite` guard's deny set; the ack flag is required on every write to it, not just the first.
|
|
95
|
+
|
|
96
|
+
10. **Verify by re-reading `.env`** and confirming the new line is there exactly once and the value matches what you wrote.
|
|
97
|
+
|
|
98
|
+
11. **Ask before restart**, same prompt as the API-key path:
|
|
99
|
+
|
|
100
|
+
> Auth is on disk. The container needs to restart to load it (TUI will briefly disconnect). May I restart now, or do you have other changes to make first?
|
|
101
|
+
|
|
102
|
+
12. On yes → call the `restart` tool. On no → tell them to run `typeclaw restart` themselves when ready.
|
|
103
|
+
|
|
104
|
+
13. **Done.** There is no auth scratch directory, no tmux session to tear down, no worktree. The OAuth path has the same on-disk footprint as the API-key path: one new line in `.env`.
|
|
105
|
+
|
|
106
|
+
## Failure modes on the user's side
|
|
107
|
+
|
|
108
|
+
These all surface as the user's reply being an error message instead of a token. Recognize them, do not validate them as tokens, and respond with the matching guidance.
|
|
109
|
+
|
|
110
|
+
- **"command not found: claude"** — they don't have the CLI installed locally. Point them at `claude.com/code` and ask them to `claude login` after installing.
|
|
111
|
+
- **"Not logged in"** / `setup-token` immediately asking them to log in — they have the CLI but no active subscription session. Have them run `claude login` first, then re-try `claude setup-token`.
|
|
112
|
+
- **"This account doesn't have access to a paid Claude subscription"** — they're on a free account. `setup-token` requires Pro / Max / Team / Enterprise. They either upgrade or use the API-key path.
|
|
113
|
+
- **"Token request failed"** / generic network error during `setup-token` — their local machine couldn't reach `claude.ai` or `api.anthropic.com`. Check VPN, firewall, corporate proxy. Re-try in a moment.
|
|
114
|
+
- **Browser opened but no token appeared in terminal** — they authorized in the wrong account, or they closed the tab before the callback completed. Have them run `setup-token` again and wait for the terminal to finish.
|
|
115
|
+
- **They report success but pasted a string that fails validation** — most likely they pasted the surrounding output (the `export` line, a banner, instructions) rather than just the token. Re-ask, emphasize "just the token, no surrounding text".
|
|
116
|
+
|
|
117
|
+
## Failure modes after you've written the token
|
|
118
|
+
|
|
119
|
+
- **`typeclaw restart` fails or the container won't come up** — the credential is on disk, the restart is the problem. Don't re-prompt for auth; surface the restart failure and tell the user to run `typeclaw restart` from their host shell to see the underlying error.
|
|
120
|
+
- **`claude` invocations after restart still say "Invalid API key" / "Unauthorized"** — token validation passed locally but the credential is rejected upstream. Three likely causes:
|
|
121
|
+
1. **Token from a different account than expected.** The user has multiple Claude accounts on their local machine and `setup-token` used the wrong one. Have them check `claude login` and re-run `setup-token` from the right account.
|
|
122
|
+
2. **`ANTHROPIC_API_KEY` is also set in `.env` and takes precedence.** Per the [auth precedence rules](https://code.claude.com/docs/en/authentication#authentication-precedence), `ANTHROPIC_API_KEY` outranks `CLAUDE_CODE_OAUTH_TOKEN`. Check `.env`; remove the stale `ANTHROPIC_API_KEY` line if the user wants OAuth.
|
|
123
|
+
3. **Token expired or revoked.** Tokens are one-year-lived; revocation happens if the user `/logout`s from the subscription that issued the token. Have them re-run `setup-token`.
|
|
124
|
+
|
|
125
|
+
## Things you must not do during auth
|
|
126
|
+
|
|
127
|
+
- **Do not run `claude setup-token` inside the container.** Use the user-machine flow above. The in-container tmux dance that this skill used to recommend has been removed; it was strictly worse than asking the user to run one command on their machine.
|
|
128
|
+
- **Do not log, echo, paste-back, or otherwise transcribe the user's token.** Not in a confirmation message, not in a sentinel, not in a commit. A one-year credential leak is significantly worse than a momentary "did you mean this?" reflection — there's no good reason for the token to leave the `.env` write path.
|
|
129
|
+
- **Do not write the token to `.env` until you've validated its format.** A malformed token quietly turns into a broken auth that surfaces only on the next claude invocation, long after the user has moved on.
|
|
130
|
+
- **Do not retry validation more than once.** If the first paste fails the regex, ask once with clearer guidance ("just the token, no surrounding text"). If the second also fails, the user is in a state that text instructions won't resolve — drop the OAuth path and recommend API-key auth.
|
|
131
|
+
- **Do not advise the user to `typeclaw shell` and run `claude setup-token` inside the container as a "fallback"**. It does not work — the container has no browser. If `setup-token` is failing on the user's machine, the right fix is on their machine (check `claude login`, check network), not switching the dance to the container.
|
|
132
|
+
- **Do not assume the token format prefix.** Anthropic has changed the prefix on long-lived tokens before (the docs use generic placeholders like `your-token`). Validate by shape (length + character class), not by prefix.
|
|
133
|
+
- **Do not write to `.env` without `acknowledgeGuards: { nonWorkspaceWrite: true }`.** Same guard contract as every other `.env` write.
|
|
134
|
+
- **Do not patch-edit `.env`.** Read-modify-write the whole file. A fragile `oldText` match could corrupt unrelated lines.
|
|
135
|
+
- **Do not branch on local-vs-remote container topology.** The user-machine flow is the same whether the container is on the user's laptop or on a remote host — the user runs `setup-token` on whatever local machine they're at, the token works in either container.
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
346
|
-
|
|
|
347
|
-
| `tmux`
|
|
348
|
-
| `gh`
|
|
349
|
-
| `python`
|
|
350
|
-
| `ffmpeg`
|
|
351
|
-
| `cjkFonts`
|
|
352
|
-
| `
|
|
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`,
|
|
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 `
|
|
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,
|
|
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.
|
package/typeclaw.schema.json
CHANGED
|
@@ -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",
|