typeclaw 0.6.0 → 0.8.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.
@@ -108,6 +108,112 @@ export const KNOWN_PROVIDERS = {
108
108
  },
109
109
  },
110
110
  },
111
+ // Anthropic Claude — both the Anthropic Console API (ANTHROPIC_API_KEY)
112
+ // and Claude Pro/Max/Team/Enterprise subscriptions (OAuth) reach the same
113
+ // /v1/messages endpoint and share one provider id. Auth path determines
114
+ // which headers pi-ai's `anthropic-messages` transport injects: API key
115
+ // sends a plain `x-api-key`; OAuth sends Bearer + Claude Code identity
116
+ // (anthropic-beta: claude-code-20250219,oauth-2025-04-20 +
117
+ // user-agent: claude-cli/<version>), which is exactly the surface a
118
+ // subscriber's `claude setup-token` credential authorizes. The OAuth dance
119
+ // itself is authorization-code + PKCE against `claude.ai/oauth/authorize`
120
+ // with a localhost callback server (not device-code); the existing
121
+ // `typeclaw-claude-code` skill documents the user-side flow for getting
122
+ // a subscription credential onto the agent when the in-container browser
123
+ // callback can't reach the user's machine.
124
+ //
125
+ // anthropic is the FIRST provider in the registry where both auth modes
126
+ // coexist on one entry. The runtime in src/agent/auth.ts has a load-bearing
127
+ // resolution rule: when secrets.json#providers.anthropic carries an OAuth
128
+ // credential, `ANTHROPIC_API_KEY` in .env is IGNORED (OAuth-on-disk wins
129
+ // because env-wins only applies to api-key-shaped credentials). For
130
+ // api-key-only providers this is invisible; for anthropic it surfaces as
131
+ // "I added the env var but the agent still uses OAuth." The mitigation is
132
+ // to remove the OAuth credential explicitly (`typeclaw provider remove
133
+ // anthropic`) before relying on the env-var path. Same rule applies to any
134
+ // future dual-auth provider — keep the surprise in mind when expanding.
135
+ //
136
+ // Model lineup is the current GA tier as of 2026-04-16: Opus 4.7 (top,
137
+ // released Apr 16 2026), Sonnet 4.6 (mid, Feb 5 2026), Haiku 4.5 (fast,
138
+ // Oct 1 2025). Anthropic's own model overview lists these three as the
139
+ // current recommended set and flags earlier Opus/Sonnet variants with
140
+ // "Consider migrating to current models." Opus 4 / Sonnet 4 are deprecated
141
+ // (retirement: Jun 15 2026); the 4.5/4.6 alternates remain Active but are
142
+ // not the recommended path.
143
+ //
144
+ // ID semantics differ across the lineup and matter for forward-compat:
145
+ // - `claude-haiku-4-5` is a 4.5-generation CONVENIENCE ALIAS that
146
+ // resolves to the latest dated snapshot (currently `-20251001`). Per
147
+ // Anthropic's model-id docs, pre-4.6 dateless ids are evergreen
148
+ // pointers — Anthropic can ship a new dated snapshot under the same
149
+ // alias and we pick it up automatically.
150
+ // - `claude-sonnet-4-6` and `claude-opus-4-7` are 4.6+-generation PINNED
151
+ // SNAPSHOTS, not aliases. Anthropic explicitly says "the dateless ID is
152
+ // the canonical model ID for that release. It maps to a single, fixed
153
+ // model snapshot." A future Sonnet 4.6.1 (if it ever exists) would ship
154
+ // under a new id, NOT silently replace `claude-sonnet-4-6`.
155
+ // Consequence for refresh discipline: bumping Haiku is a no-op (alias
156
+ // catches the latest); bumping Sonnet/Opus to a future 4.7+ family is a
157
+ // real edit here. Don't assume `claude-opus-4-7` will silently advance.
158
+ //
159
+ // Opus 4.7 specifics that affect cost accounting:
160
+ // - New tokenizer: same input maps to 1.0-1.3x more tokens than prior
161
+ // generations depending on content type. Per-token price is unchanged
162
+ // vs Opus 4.6, but total cost on identical workloads can rise meaningfully.
163
+ // - 1M token context window (vs 200k on Haiku) and 128k max output (vs
164
+ // 64k on Sonnet/Haiku). 1M context is at standard pricing — no surcharge.
165
+ // - New `xhigh` effort level between `high` and `max` (pi-ai 0.67.x may
166
+ // not surface this knob yet; check before relying on it).
167
+ //
168
+ // Pricing mirrors Anthropic's official table as of 2026-05; cacheWrite is
169
+ // the 5m-TTL rate (1.25x input). 1h TTL is ~2x input (not modeled here —
170
+ // pi-ai's `cacheWrite` field captures the default 5m rate only).
171
+ anthropic: {
172
+ id: 'anthropic',
173
+ name: 'Anthropic',
174
+ baseUrl: 'https://api.anthropic.com',
175
+ auth: ['api-key', 'oauth'],
176
+ apiKeyEnv: 'ANTHROPIC_API_KEY',
177
+ oauthProviderId: 'anthropic',
178
+ models: {
179
+ 'claude-haiku-4-5': {
180
+ id: 'claude-haiku-4-5',
181
+ name: 'Claude Haiku 4.5',
182
+ api: 'anthropic-messages',
183
+ provider: 'anthropic',
184
+ baseUrl: 'https://api.anthropic.com',
185
+ reasoning: true,
186
+ input: ['text', 'image'],
187
+ cost: { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
188
+ contextWindow: 200000,
189
+ maxTokens: 64000,
190
+ },
191
+ 'claude-sonnet-4-6': {
192
+ id: 'claude-sonnet-4-6',
193
+ name: 'Claude Sonnet 4.6',
194
+ api: 'anthropic-messages',
195
+ provider: 'anthropic',
196
+ baseUrl: 'https://api.anthropic.com',
197
+ reasoning: true,
198
+ input: ['text', 'image'],
199
+ cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
200
+ contextWindow: 1000000,
201
+ maxTokens: 64000,
202
+ },
203
+ 'claude-opus-4-7': {
204
+ id: 'claude-opus-4-7',
205
+ name: 'Claude Opus 4.7',
206
+ api: 'anthropic-messages',
207
+ provider: 'anthropic',
208
+ baseUrl: 'https://api.anthropic.com',
209
+ reasoning: true,
210
+ input: ['text', 'image'],
211
+ cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
212
+ contextWindow: 1000000,
213
+ maxTokens: 128000,
214
+ },
215
+ },
216
+ },
111
217
  // ChatGPT Plus/Pro subscription via the OAuth Codex backend. No API key
112
218
  // path here on purpose — the Codex backend is OAuth-only upstream.
113
219
  //
@@ -394,14 +394,101 @@ RUN echo "${encoded}" | base64 -d > ${TYPECLAW_ENTRYPOINT_PATH} \\
394
394
  // `~/.local/bin/claude` shim, which itself dereferences to the versioned
395
395
  // binary under `~/.local/share/claude/versions/<ver>/`, so upgrades via
396
396
  // `claude update` keep working without re-running this layer.
397
+ // `~/.claude.json` is Claude Code's internal state file (NOT
398
+ // `~/.claude/settings.json`, which is user-facing). On first run with an
399
+ // empty or missing file, `claude` enters a TTY-only theme picker:
400
+ // "Welcome to Claude Code … Choose the text style that looks best with
401
+ // your terminal" with 7 options. The picker is unskippable via CLI
402
+ // flags or env vars (no `--skip-onboarding`, no `--theme=dark`;
403
+ // `IS_DEMO=1` exists but has documented side effects). The single
404
+ // official escape hatch is writing `{"hasCompletedOnboarding": true,
405
+ // "theme": "dark"}` to `~/.claude.json` before the first launch —
406
+ // confirmed by Anthropic in multiple GitHub issues
407
+ // (anthropics/claude-code#4714, #8938, #13827) and the empirical
408
+ // answer used by metabase/metabase's `bin/claude-dangerous`, the
409
+ // `claudeCodeAlDevContainer` feature, and dozens of other Docker
410
+ // integrations.
411
+ //
412
+ // Without the pre-seed, the very first agent-driven `tmux new-session …
413
+ // claude` invocation hangs on the theme picker: the agent's
414
+ // `send-keys "<prompt>" Enter` arrives at the picker, gets interpreted
415
+ // as picker input, and never reaches claude's actual prompt. The
416
+ // `typeclaw-claude-code` skill is structured around a `Stop`-hook
417
+ // sentinel, which never fires while the picker is up, so the polling
418
+ // loop only learns of the hang at the 10-minute wall-clock budget.
419
+ // Pre-seeding here costs ~85 bytes on disk and zero runtime overhead.
420
+ //
421
+ // SCOPE: this seed is NECESSARY but not SUFFICIENT for a fully
422
+ // no-questions-asked first launch. Claude Code also shows two
423
+ // post-seed modal dialogs that this file deliberately does NOT
424
+ // pre-clear:
425
+ // 1. "Detected a custom API key from environment. Do you want to use
426
+ // this API key?" — fires when ANTHROPIC_API_KEY is set. Options
427
+ // `[No (recommended), Yes]`, focus on No, picker does NOT wrap.
428
+ // 2. Workspace trust ("Do you trust the files in this folder?") —
429
+ // fires on every new cwd. Options `[Yes, proceed, No, exit]`,
430
+ // focus on Yes.
431
+ // Both are kept as runtime decisions handled by the
432
+ // `typeclaw-claude-code` skill (see its "Driving the session" section,
433
+ // "Clear startup dialogs" step, which uses dialog-specific keystrokes
434
+ // because the picker doesn't wrap). Pre-seeding
435
+ // `hasTrustDialogAccepted` or `customApiKeyResponses.approved` here
436
+ // would silently widen the trust surface in ways the operator hasn't
437
+ // consented to — the seed's job is strictly cosmetic-wizard removal,
438
+ // not trust/permission preemption.
439
+ //
440
+ // `theme: "dark"` matches typeclaw's default TUI theme so the visual
441
+ // transition between the typeclaw TUI and a tmux-attached claude pane
442
+ // is consistent. Users on light terminals can override by editing
443
+ // `~/.claude.json` (which persists across container restarts only if
444
+ // they mount it; in the default container-ephemeral state it resets
445
+ // to this default on every rebuild, which is fine — `claude` reads
446
+ // the file at startup and the theme has no behavioral impact).
447
+ //
448
+ // `lastOnboardingVersion` is INTENTIONALLY OMITTED. ii-agent and a
449
+ // few other templates ship `lastOnboardingVersion: "1.0.30"`, but
450
+ // that value is version-coupled and goes stale on every Claude Code
451
+ // release. Empirically against Claude Code 2.1.146, the current
452
+ // `hasCompletedOnboarding: true` alone is honored without a version
453
+ // pin. If a future Claude version starts re-triggering the picker
454
+ // when the field is missing, capture `claude --version` output at
455
+ // build time and inject it then — don't hardcode a stale value.
456
+ //
457
+ // `installMethod: "native"` and `numStartups: 1` match the shape
458
+ // Claude Code itself writes after a clean first launch; keeping them
459
+ // makes our seed indistinguishable from a real post-onboarding state,
460
+ // which minimizes the chance of a future "if the file looks like
461
+ // agent-pre-seed, redo onboarding" detection heuristic landing on us.
462
+ //
463
+ // Built via `JSON.stringify` rather than a hand-written string
464
+ // literal so quote/escape bugs surface as TS errors at compile time,
465
+ // not as a corrupt `~/.claude.json` discovered only when the build
466
+ // runs. The `printf '%s\\n' '<JSON>'` shell pattern relies on the
467
+ // JSON containing no single quotes (true by construction — JSON.
468
+ // stringify only emits double quotes); a regression test parses the
469
+ // emitted JSON back to confirm.
470
+ const CLAUDE_CODE_ONBOARDING_SEED = JSON.stringify({
471
+ hasCompletedOnboarding: true,
472
+ theme: 'dark',
473
+ installMethod: 'native',
474
+ numStartups: 1,
475
+ })
476
+
397
477
  function renderClaudeCodeInstallLayer(enabled: boolean): string {
398
478
  if (!enabled) return ''
399
479
  return `# Layer 5.6 (toggle): install Anthropic's Claude Code CLI. Opt-in via
400
480
  # typeclaw.json#docker.file.claudeCode. The skill \`typeclaw-claude-code\`
401
- # documents the auth + usage flow.
481
+ # documents the auth + usage flow. Pre-seed ~/.claude.json so the first
482
+ # launch skips the TTY-only theme picker; see CLAUDE_CODE_ONBOARDING_SEED
483
+ # above for the rationale and what the seed deliberately does NOT cover.
484
+ # The seed write runs LAST in the chain so the final layer state is
485
+ # exactly the seeded config — independent of whether any earlier command
486
+ # (or a future Claude version's \`--version\` smoke test) writes a
487
+ # default \`~/.claude.json\` partway through the layer.
402
488
  RUN curl -fsSL https://claude.ai/install.sh | bash \\
403
489
  && ln -sf "$HOME/.local/bin/claude" /usr/local/bin/claude \\
404
- && claude --version > /dev/null`
490
+ && claude --version > /dev/null \\
491
+ && printf '%s\\n' '${CLAUDE_CODE_ONBOARDING_SEED}' > "$HOME/.claude.json"`
405
492
  }
406
493
 
407
494
  // Shared-library runtime deps Chrome for Testing needs to launch on amd64
@@ -13,6 +13,7 @@ const PROVIDER_TO_MODELS_DEV: Record<KnownProviderId, string> = {
13
13
  // (Codex is a backend, not a separate provider in their taxonomy). Curated
14
14
  // entries are surfaced regardless of upstream membership.
15
15
  'openai-codex': 'openai',
16
+ anthropic: 'anthropic',
16
17
  fireworks: 'fireworks-ai',
17
18
  zai: 'zai',
18
19
  // zai-coding (GLM Coding Plan) is a billing surface, not a separate model
@@ -21,4 +21,4 @@ export {
21
21
  type TunnelSnapshot,
22
22
  } from './protocol'
23
23
 
24
- export { formatLocalDate, formatLocalDateTime } from './local-time'
24
+ export { formatLocalDate, formatLocalDateTime, resolveLocalTimezoneName } from './local-time'
@@ -19,3 +19,20 @@ function formatTimezoneOffset(date: Date): string {
19
19
  const abs = Math.abs(offsetMinutes)
20
20
  return `${sign}${pad2(Math.floor(abs / 60))}:${pad2(abs % 60)}`
21
21
  }
22
+
23
+ // IANA timezone name of the process (e.g. `Asia/Seoul`). Reads the resolved
24
+ // zone from Intl, falling back to `UTC` if the runtime cannot resolve one —
25
+ // this should never happen on Bun + tzdata-equipped containers, but the
26
+ // fallback keeps the prompt renderable rather than throwing during session
27
+ // creation. The returned name is what the agent shows the user when asked
28
+ // "what time is it" — pairing the wall clock with a recognizable zone name
29
+ // is what disambiguates "15:31 +09:00" from "15:31 KST" for a non-technical
30
+ // reader.
31
+ export function resolveLocalTimezoneName(): string {
32
+ try {
33
+ const zone = Intl.DateTimeFormat().resolvedOptions().timeZone
34
+ return zone && zone.length > 0 ? zone : 'UTC'
35
+ } catch {
36
+ return 'UTC'
37
+ }
38
+ }
@@ -9,6 +9,12 @@ You can delegate work to Claude Code, Anthropic's official coding agent. The age
9
9
 
10
10
  This skill is for the case where Claude Code is the right tool: hard architecture work, multi-file refactors, deep code analysis, a second-opinion read on something you wrote. It is **not** for trivial edits — the round-trip cost (worktree setup + process spawn + auth check + TUI init + at least one full Claude turn) is 15–45 seconds and several thousand tokens of someone else's context window. Do trivial edits yourself.
11
11
 
12
+ ## Run the delegation inside `operator`, not inline
13
+
14
+ Once you've decided Claude Code is the right tool, spawn the bundled `operator` subagent to do the actual driving — don't run the worktree setup, the tmux session, the polling loop, the multi-turn decision loop, and the cleanup inline in your own context. The whole loop typically takes several minutes and produces large amounts of intermediate output (TUI buffer captures, Stop sentinels per turn, JSONL transcript references); running it inline blocks the user from talking to you and burns through your context window before you ever get to the synthesis step. `operator` is write-capable and runs the same loop, then returns a clean final report (what claude produced, what `git diff main..cc-<id>` shows, what you should review). You ship the worktree, the prompt, and the safety constraints to operator; operator ships you back the diff and the summary.
15
+
16
+ Exception: a quick sanity ping (`claude --version` to check the binary exists, `env | grep ANTHROPIC` to check auth). Those are single fast bash calls — do them inline. The "spawn through operator" rule applies to anything that runs `claude` itself as an interactive TUI.
17
+
12
18
  ## When to delegate to Claude Code
13
19
 
14
20
  Use Claude Code for:
@@ -79,6 +85,7 @@ Before you spawn `claude` for any real work:
79
85
  - **`docker.file.claudeCode: true`** in `typeclaw.json`. Verify with `which claude`; if missing, the toggle isn't on. Tell the user to enable it and `typeclaw start --build`.
80
86
  - **`docker.file.tmux: true`** (default `true`, but check). Verify with `which tmux`.
81
87
  - **Auth set up** — see above. Verify with `env | grep -E '^(ANTHROPIC_API_KEY|CLAUDE_CODE_OAUTH_TOKEN)='`.
88
+ - **Onboarding pre-seeded.** The Dockerfile layer writes `~/.claude.json` with `hasCompletedOnboarding: true` and `theme: "dark"` so the first `claude` invocation skips the TTY-only theme picker / welcome wizard. **This is necessary but not sufficient** — even with the seed, Claude Code can still land on two other pre-prompt modals: the "Detected a custom API key from environment. Do you want to use this API key?" confirmation (when `ANTHROPIC_API_KEY` is set in env — default focus is **No**, so `Down Enter` is needed to accept) and the workspace trust dialog ("Do you trust the files in this folder?", default focus already on **Yes**, so a bare `Enter` accepts). The "Driving the session" section below clears them as a loop. If `~/.claude.json` is empty or missing entirely (custom mount, manual `rm`, a `CLAUDE_CONFIG_DIR` pointing at a fresh directory), the theme picker also reappears. Self-heal: `printf '%s\n' '{"hasCompletedOnboarding":true,"theme":"dark","installMethod":"native","numStartups":1}' > "$HOME/.claude.json"` before spawning, then retry.
82
89
  - **Agent folder is a git repo.** Verify with `git -C /agent rev-parse --is-inside-work-tree`. The worktree model below requires it. If the user's agent folder somehow isn't a repo (rare — `typeclaw init` scaffolds one), tell them to `git init && git add -A && git commit -m "initial"` first.
83
90
  - **No uncommitted changes that you care about.** `git -C /agent status --porcelain` should be clean, or you should be willing to set the working tree aside before delegating. The worktree is a separate checkout, so claude can't see your uncommitted changes — meaning claude operates on the last committed state. If the user wants claude to work with in-progress edits, commit them first (even on a WIP branch).
84
91
 
@@ -165,11 +172,29 @@ The minimum protocol — translate to your actual tool calls:
165
172
  1. Create the worktree, write the hook config (above).
166
173
  2. `tmux new-session -d -s cc-<id> -c /tmp/cc-<id> claude`.
167
174
  3. Wait ~3 seconds for the TUI to initialize.
168
- 4. `tmux send-keys -t cc-<id> "<your prompt>" Enter`.
169
- 5. **Poll** for `/tmp/cc-<id>/.done` in a 500ms-cadence loop with a wall-clock budget (default 10 minutes). On every iteration, also check `tmux has-session -t cc-<id>` — if the session died, claude crashed or auth failed.
170
- 6. When `.done` exists: `rm .done`, read `sentinel.json`, examine `last_assistant_message`.
171
- 7. Decide using the multi-turn loop below.
172
- 8. When done: `tmux send-keys -t cc-<id> "/exit" Enter && sleep 1 && tmux kill-session -t cc-<id>`.
175
+ 4. **Clear startup dialogs (BEFORE sending the task prompt).** Even with `~/.claude.json` pre-seeded, claude can land on one or both pre-prompt modals. Run this as a **loop**, not a one-shot: clearing one dialog can immediately reveal the next, and you must keep polling until claude's actual input prompt is visible (it renders a bottom-of-pane input box with a `╭` / `╰` border).
176
+
177
+ The two known modals, with the exact keystrokes for each (Claude Code's select widget does NOT wrap — pressing `Up` from the first option is a no-op, so the direction must match the dialog's option order):
178
+ - **Custom API key confirmation** — "Detected a custom API key from environment. Do you want to use this API key?" Fires when `ANTHROPIC_API_KEY` is set (exactly typeclaw's auth path). Options are `[No (recommended), Yes]` with focus initialized on **No**. Resolution: `tmux send-keys -t cc-<id> Down Enter` to advance to **Yes** and submit. Sending `Up Enter` would submit the **No** answer, which can persist as a rejection in `customApiKeyResponses.rejected` and break subsequent launches — never do that here.
179
+
180
+ - **Workspace trust** — "Do you trust the files in this folder?" Fires on first launch in any new cwd, so every fresh `/tmp/cc-<id>/` worktree triggers it. Options are `[Yes, proceed, No, exit]` with focus on the first option (**Yes**) by default. Resolution: bare `tmux send-keys -t cc-<id> Enter` — no arrow key needed. Always verify the pane text matches the trust dialog before pressing Enter; a misidentified modal would submit a different default.
181
+
182
+ Loop shape (translate to your tool calls):
183
+ 1. Capture the last ~15 lines: `tmux capture-pane -t cc-<id> -p -S -15`.
184
+ 2. If the capture contains the API key dialog text → `send-keys Down Enter`, sleep 500ms, goto 1.
185
+ 3. If the capture contains the trust dialog text → `send-keys Enter`, sleep 500ms, goto 1.
186
+ 4. If the capture shows the input box (`╭` border on a bottom line, no dialog text above it) → ready; exit the loop.
187
+ 5. Otherwise sleep 500ms, goto 1. Apply a wall-clock budget of ~10 seconds; if the loop hasn't reached step 4 by then, abort with `/exit` and surface to the user — claude is in a state this skill doesn't model.
188
+
189
+ Do not use a fixed 2-second wait then send the prompt — cold-start and slow-disk cases can deliver a dialog at 2.5s+, and sending the task prompt into a modal corrupts the session.
190
+
191
+ **Safety note**: accepting workspace trust on a fresh `/tmp/cc-<id>/` worktree is the right call **only when its `HEAD` is the intended clean state** — typically the agent folder's last good commit on a branch the user controls. If the user just merged a third-party PR, pulled a remote branch, or checked out an untrusted ref, the worktree carries that content too and "trusting" it gives claude tool access on potentially hostile code. Before auto-accepting trust, sanity-check: if the user hasn't said something equivalent to "delegate this to Claude Code", or if you're not confident the current `HEAD` is one the user authored or reviewed, surface the trust dialog to them instead. Do NOT extend even a legitimate trust acceptance to in-session permission prompts (Bash, Edit, etc.) — those still need per-turn judgment per the multi-turn decision loop below.
192
+
193
+ 5. `tmux send-keys -t cc-<id> "<your prompt>" Enter`.
194
+ 6. **Poll** for `/tmp/cc-<id>/.done` in a 500ms-cadence loop with a wall-clock budget (default 10 minutes). On every iteration, also check `tmux has-session -t cc-<id>` — if the session died, claude crashed or auth failed.
195
+ 7. When `.done` exists: `rm .done`, read `sentinel.json`, examine `last_assistant_message`.
196
+ 8. Decide using the multi-turn loop below.
197
+ 9. When done: `tmux send-keys -t cc-<id> "/exit" Enter && sleep 1 && tmux kill-session -t cc-<id>`.
173
198
 
174
199
  The full polling implementation, the ANSI-handling rules for `capture-pane` fallbacks, and the "tmux session died unexpectedly" recovery path are in `references/tmux-driving.md`.
175
200
 
@@ -342,17 +342,17 @@ The `docker.file` block has two layers of customization:
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
- | `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`. |
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) and pre-seeds `~/.claude.json` to skip the TTY-only theme picker on first launch (without it the agent's `tmux send-keys` would be eaten by the picker). 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 including how to clear the post-seed API-key/trust dialogs. |
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`. |
356
356
 
357
357
  Toggle version strings reject whitespace and `=` (apt-injection guard) — pass just the version, not `pkg=ver`.
358
358
 
@@ -427,7 +427,7 @@ The toggle-driven apt install benefits from BuildKit `--mount=type=cache` on `/v
427
427
 
428
428
  ## Gitignore
429
429
 
430
- `typeclaw start` rewrites the agent folder's `.gitignore` from a template baked into the typeclaw CLI on **every** invocation, then auto-commits it when the agent folder is a git repo and the file changed. The template protects two categories: truly-ignored paths (`.env`, `node_modules/`, `workspace/`, `mounts/`, `Dockerfile`, `.DS_Store`) and system-managed runtime state (`sessions/`, `memory/`, `channels/`) that TypeClaw, not the agent, commits on its own schedule. Editing `.gitignore` by hand is temporary; the next `typeclaw start` overwrites it.
430
+ `typeclaw start` rewrites the agent folder's `.gitignore` from a template baked into the typeclaw CLI on **every** invocation, then auto-commits it when the agent folder is a git repo and the file changed. The template protects two categories: truly-ignored paths (`secrets.json`, `.env`, `.env.local`, `auth.json`, `node_modules/`, `workspace/`, `mounts/`, `Dockerfile`, `.DS_Store`) and system-managed runtime state (`sessions/`, `memory/`, `channels/`) that TypeClaw, not the agent, commits on its own schedule. Editing `.gitignore` by hand is temporary; the next `typeclaw start` overwrites it.
431
431
 
432
432
  The `git.ignore.append` field (introduced when the legacy top-level `gitignore` key was nested under the `git` namespace for future extensibility — see **Legacy migration**) is the supported escape hatch for additional local ignore patterns. It is an array of strings, each treated as a single `.gitignore` line. The CLI splices them into the autogenerated `.gitignore` before TypeClaw's protected rules, prefixed with a `# Custom entries from typeclaw.json#git.ignore.append.` comment.
433
433
 
@@ -439,7 +439,7 @@ The `git.ignore.append` field (introduced when the legacy top-level `gitignore`
439
439
 
440
440
  ### Ordering and protected paths
441
441
 
442
- `.gitignore` is order-sensitive: later `!` negation rules can unignore earlier ignore rules. TypeClaw therefore renders `git.ignore.append` **before** its own truly-ignored and system-managed entries, so even a custom `!sessions/` or `!.env` cannot override TypeClaw's protections. Custom ordinary ignore patterns still work because they add additional ignores; they just do not get the final word over TypeClaw-owned paths.
442
+ `.gitignore` is order-sensitive: later `!` negation rules can unignore earlier ignore rules. TypeClaw therefore renders `git.ignore.append` **before** its own truly-ignored and system-managed entries, so even a custom `!sessions/`, `!secrets.json`, or `!.env` cannot override TypeClaw's protections. Custom ordinary ignore patterns still work because they add additional ignores; they just do not get the final word over TypeClaw-owned paths.
443
443
 
444
444
  Materialized shape when `append` is non-empty:
445
445
 
@@ -449,6 +449,7 @@ scratch/
449
449
  *.local.log
450
450
 
451
451
  # Truly ignored: ...
452
+ secrets.json
452
453
  .env
453
454
  Dockerfile
454
455
 
@@ -514,16 +515,16 @@ Do **not** invent plugin blocks; their existence is determined by the plugins li
514
515
 
515
516
  The model registry currently has these entries:
516
517
 
517
- | `model` value | Display name | Provider | Auth | Notes |
518
- | ------------------------------------------------------ | --------------- | ------------ | ------------------- | ---------------------------------------------------------------------------------------- |
519
- | `openai/gpt-5.4-nano` | GPT-5.4 nano | OpenAI | API key | Default. Requires `OPENAI_API_KEY` in `.env`. Reasoning model, 400K context. |
520
- | `openai/gpt-5.4-mini` | GPT-5.4 mini | OpenAI | API key | Requires `OPENAI_API_KEY` in `.env`. Reasoning model, 400K context. |
521
- | `openai/gpt-5.4` | GPT-5.4 | OpenAI | API key | Requires `OPENAI_API_KEY` in `.env`. Reasoning model, 1.05M context. |
522
- | `openai/gpt-5.5` | GPT-5.5 | OpenAI | API key | Flagship. Requires `OPENAI_API_KEY` in `.env`. Reasoning model, 1.05M context. |
523
- | `openai-codex/gpt-5.4-mini` | GPT-5.4 mini | OpenAI Codex | OAuth (ChatGPT P/P) | Cheaper Codex tier. Requires OAuth login at init. Persisted to `secrets.json`. 272K ctx. |
524
- | `openai-codex/gpt-5.4` | GPT-5.4 | OpenAI Codex | OAuth (ChatGPT P/P) | Codex mid-tier. Requires OAuth login at init. Persisted to `secrets.json`. 272K context. |
525
- | `openai-codex/gpt-5.5` | GPT-5.5 | OpenAI Codex | OAuth (ChatGPT P/P) | Flagship Codex. Requires OAuth login at init. Persisted to `secrets.json`. 272K context. |
526
- | `fireworks/accounts/fireworks/routers/kimi-k2p6-turbo` | Kimi K2.6 Turbo | Fireworks | API key | Requires `FIREWORKS_API_KEY` in `.env`. Reasoning model, 256K context. |
518
+ | `model` value | Display name | Provider | Auth | Notes |
519
+ | ------------------------------------------------------ | --------------- | ------------ | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
520
+ | `openai/gpt-5.4-nano` | GPT-5.4 nano | OpenAI | API key | Default. API key in `secrets.json#providers.openai.key.value` (or `OPENAI_API_KEY` env override). Reasoning model, 400K context. |
521
+ | `openai/gpt-5.4-mini` | GPT-5.4 mini | OpenAI | API key | API key in `secrets.json#providers.openai.key.value` (or `OPENAI_API_KEY` env override). Reasoning model, 400K context. |
522
+ | `openai/gpt-5.4` | GPT-5.4 | OpenAI | API key | API key in `secrets.json#providers.openai.key.value` (or `OPENAI_API_KEY` env override). Reasoning model, 1.05M context. |
523
+ | `openai/gpt-5.5` | GPT-5.5 | OpenAI | API key | Flagship. API key in `secrets.json#providers.openai.key.value` (or `OPENAI_API_KEY` env override). Reasoning model, 1.05M context. |
524
+ | `openai-codex/gpt-5.4-mini` | GPT-5.4 mini | OpenAI Codex | OAuth (ChatGPT P/P) | Cheaper Codex tier. Requires OAuth login at init. Persisted to `secrets.json`. 272K ctx. |
525
+ | `openai-codex/gpt-5.4` | GPT-5.4 | OpenAI Codex | OAuth (ChatGPT P/P) | Codex mid-tier. Requires OAuth login at init. Persisted to `secrets.json`. 272K context. |
526
+ | `openai-codex/gpt-5.5` | GPT-5.5 | OpenAI Codex | OAuth (ChatGPT P/P) | Flagship Codex. Requires OAuth login at init. Persisted to `secrets.json`. 272K context. |
527
+ | `fireworks/accounts/fireworks/routers/kimi-k2p6-turbo` | Kimi K2.6 Turbo | Fireworks | API key | API key in `secrets.json#providers.fireworks.key.value` (or `FIREWORKS_API_KEY` env override). Reasoning model, 256K context. |
527
528
 
528
529
  **Do not write any other value into `model`.** The schema enum will reject the file at load, and the runtime will refuse to boot the agent process. If the user names a model that isn't in this table — "use Claude", "switch to o3" — be honest:
529
530
 
@@ -533,12 +534,9 @@ Do **not** edit `typeclaw.json` to a model the registry doesn't know, even if th
533
534
 
534
535
  ## Provider credentials
535
536
 
536
- `typeclaw.json` does **not** hold API keys or OAuth tokens. Credentials live in two gitignored files:
537
+ `typeclaw.json` does **not** hold API keys or OAuth tokens. Credentials live in two gitignored files, with `secrets.json` as the canonical store and `.env` retained for env-var overrides and parity with non-typeclaw tooling that reads from the environment:
537
538
 
538
- - **`./.env`** (any environment variable, including API keys): plain `KEY=value` lines, loaded by Docker via `--env-file` at container start. The canonical env-var names per provider:
539
- - `OPENAI_API_KEY` — for any `openai/...` model.
540
- - `FIREWORKS_API_KEY` — for any `fireworks/...` model.
541
- - **`./secrets.json`** (structured store): a `v2` envelope managed by `SecretsBackend` (wraps `pi-coding-agent`'s `AuthStorage`). Two top-level slices:
539
+ - **`./secrets.json`** (canonical structured store): a `v2` envelope managed by `SecretsBackend` (wraps `pi-coding-agent`'s `AuthStorage`). Written by `typeclaw init`, the OAuth refresh path, and explicit user-driven rotation. Two top-level slices:
542
540
  - `providers.*` — per-provider credentials. API-key providers store `{ type: 'api_key', key: <Secret> }`. OAuth providers store the `pi-coding-agent` token blob `{ type: 'oauth', access_token, refresh_token, expires_at, ... }`. The container auto-refreshes OAuth tokens with file locking; api-key writes only happen on explicit user-driven rotation.
543
541
  - `channels.*` — per-adapter credentials, with named fields per adapter:
544
542
  - `discord-bot: { token: <Secret> }`
@@ -547,6 +545,13 @@ Do **not** edit `typeclaw.json` to a model the registry doesn't know, even if th
547
545
 
548
546
  (Pre-v2 agent folders carry the older `llm` slice and channel-env-var-keyed shape; they are upgraded transparently on first read. Pre-rename folders may even carry the file as `auth.json`; it is renamed to `secrets.json` on the next boot.)
549
547
 
548
+ - **`./.env`** (env-var overrides): plain `KEY=value` lines, loaded by Docker via `--env-file` at container start. When set, an env var **wins** over the file value (see resolution rules below). Useful for CI, transient rotations, or any tooling outside typeclaw that reads from the environment. The canonical env-var names per provider:
549
+ - `OPENAI_API_KEY` — for any `openai/...` model.
550
+ - `FIREWORKS_API_KEY` — for any `fireworks/...` model.
551
+ - `ANTHROPIC_API_KEY` — for any `anthropic/...` model when using API-key auth.
552
+
553
+ New typeclaw secrets should land in `secrets.json` (via `typeclaw init` or a structured edit) — `.env` is no longer the default home.
554
+
550
555
  ### The `Secret` shape and env-wins resolution
551
556
 
552
557
  Every secret-bearing field in `secrets.json` is a **`Secret`**: either a plain string or an object `{ value?, env? }`.
@@ -580,11 +585,11 @@ Every secret-bearing field in `secrets.json` is a **`Secret`**: either a plain s
580
585
 
581
586
  ### Switching credentials
582
587
 
583
- If a user wants to switch from API key to OAuth (or vice versa) for a provider that supports both, the easiest path is to delete the relevant entry from `.env` / `secrets.json#providers` and re-run `typeclaw init` from inside the agent folder — it'll prompt for the auth method again.
588
+ If a user wants to switch from API key to OAuth (or vice versa) for a provider that supports both, the easiest path is to delete the relevant entry from `secrets.json#providers` (and any matching env-var override in `.env`) and re-run `typeclaw init` from inside the agent folder — it'll prompt for the auth method again.
584
589
 
585
- If the user wants to rotate an api-key, edit either `.env` (env-wins picks it up immediately) or `secrets.json#providers.<provider>.key` (rewrite the `value` field, or remove the entry if the env var should take over). After either, `typeclaw restart` on the host stage.
590
+ If the user wants to rotate an api-key, edit `secrets.json#providers.<provider>.key` rewrite the `value` field (preserving any `env` binding), or remove the entry entirely if an env-var override is taking over. `.env` is a secondary path that still works (env-wins picks it up immediately), but `secrets.json` is the durable home. After either, `typeclaw restart` on the host stage.
586
591
 
587
- Never echo, log, or commit values from `.env` or `secrets.json`. Both are gitignored by default — keep them that way.
592
+ Never echo, log, or commit values from `secrets.json` or `.env`. Both are gitignored by default — keep them that way.
588
593
 
589
594
  ## Editing `typeclaw.json` safely
590
595
 
@@ -627,7 +632,7 @@ Never echo, log, or commit values from `.env` or `secrets.json`. Both are gitign
627
632
  ## Things you must not do
628
633
 
629
634
  - **Do not invent fields the schema doesn't support** (no `provider`, `apiKey`, `temperature`, `maxTokens`, `systemPrompt`, `tools`, `timeout`, `retry`, etc.). They will be silently dropped or, worse, mistaken for a plugin config block. Lying to the user that "I added a temperature field" when the runtime ignores it is a worse failure than refusing.
630
- - **Do not move secrets into `typeclaw.json`.** It is committed to git. API keys belong in `.env`.
635
+ - **Do not move secrets into `typeclaw.json`.** It is committed to git. API keys and channel tokens belong in `secrets.json` (or, for env-override use cases, `.env`).
631
636
  - **Do not change `port` casually.** The host-stage `typeclaw start` launcher publishes a port mapping it learned at `start` time. Changing the port in `typeclaw.json` without re-running `typeclaw start` (which re-reads it) means the TUI will connect to the wrong port and silently fail. If you change `port`, tell the user explicitly that the next `typeclaw start` will pick the new mapping.
632
637
  - **Do not change `model` to something not in the registry.** The schema enum will reject the file at load, and the runtime will refuse to boot the agent process. If the user wants a model that isn't there, this is a typeclaw-side change, not a config edit.
633
638
  - **Do not edit `typeclaw.json` from inside an `exec` cron job's `command`.** That mutates the file behind the runtime's back. Live-reloadable fields still won't update until something triggers a `reload`, and restart-required fields are guaranteed wrong.
@@ -9,7 +9,7 @@ Your agent folder is a git repo. Almost every file in it (`typeclaw.json`, `cron
9
9
 
10
10
  The contents of `.gitignore` split into two distinct categories — the distinction matters for this skill:
11
11
 
12
- - **Truly ignored** (`.env`, `node_modules/`, `workspace/`, `mounts/`, `Dockerfile`, `.DS_Store`) — never in history, ever. Secrets, runtime junk, your free-write zone, and regenerated-on-start system files.
12
+ - **Truly ignored** (`secrets.json`, `.env`, `node_modules/`, `workspace/`, `mounts/`, `Dockerfile`, `.DS_Store`) — never in history, ever. Secrets, runtime junk, your free-write zone, and regenerated-on-start system files.
13
13
  - **System-managed** (`sessions/`, `memory/`, `channels/`) — gitignored so _you_ don't stage them, but TypeClaw force-commits them on its own schedule. `sessions/` is auto-backed up by the runtime; `memory/` is committed by the dreaming subagent; `channels/` is runtime-owned channel state. Treat them as runtime-owned: do not `git add` them, do not write commit messages about them, and do not be alarmed when they appear in `git log`.
14
14
 
15
15
  Everything not in either bucket is yours to commit.
@@ -80,7 +80,7 @@ If you discover an unrelated dirty file from a previous turn, commit it separate
80
80
  - **Do not skip the commit** "because the change is small." Small changes are exactly the ones that get lost. Toggling `enabled: false` on a cron job is a decision; commit it.
81
81
  - **Do not write empty or generic messages** ("update", "fix", "change config"). The history exists to be read.
82
82
  - **Do not amend or force-push** to clean up later. Sloppy history with real commits beats clean history that lies about when decisions happened.
83
- - **Do not commit `.env` or anything truly-ignored.** If `git status` shows a truly-ignored file as staged, something is wrong with `.gitignore` — fix that first, don't commit the secret.
83
+ - **Do not commit `secrets.json`, `.env`, or anything truly-ignored.** If `git status` shows a truly-ignored file as staged, something is wrong with `.gitignore` — fix that first, don't commit the secret.
84
84
  - **Do not commit `sessions/` or `memory/` either, even though `git log` shows them.** They're system-managed: TypeClaw's auto-backup and dreaming subagent own those commits. If you find one of them staged in your working tree, unstage it (`git restore --staged sessions/ memory/`) — your edit got mixed up with the runtime's domain.
85
85
  - **Do not bundle unrelated changes.** One commit, one decision.
86
86
 
@@ -718,7 +718,7 @@ Plugin `ToolContext` is `{ signal, sessionId, agentDir, logger }`. There is no `
718
718
  - `session.prompt`: `src/agent/index.ts` `createResourceLoader` (after default prompt assembly)
719
719
  - `session.idle`: `src/server/index.ts` `drain()` — fires immediately after every `session.prompt()` resolves (success or error)
720
720
  - `session.start`/`session.end`: `src/server/index.ts` ws open/close
721
- - `tool.before`/`tool.after`: `src/agent/plugin-tools.ts` `wrapPluginTool`, `wrapSystemTool`, and `wrapSystemAgentTool`
721
+ - `tool.before`/`tool.after`: `src/agent/plugin-tools.ts` `wrapPluginTool`, `wrapSystemTool`, `wrapSystemAgentTool`, and `wrapAgentToolAsCustomToolDefinition`. The last one is the load-bearing path for pi's builtin coding tools (`read`/`bash`/`edit`/`write`/`grep`/`find`/`ls`): pi-coding-agent 0.67.3 treats `createAgentSession({ tools })` as a name filter only, so the wrapping has to ride in `customTools` to actually override the builtin implementations. See the top-of-file contract block in `plugin-tools.ts` for the full reasoning.
722
722
  - **Schema additions**: `src/config/config.ts` (`plugins` array, `.catchall(z.unknown())` for per-plugin blocks, `extractPluginConfigs`)
723
723
 
724
724
  ### Audit log on boot
@@ -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",