Hublot
The oven porthole: watch the real thing cooking, in its real environment, without opening the door and changing what you are measuring.
A Claude can drive a genuine interactive Claude Code session — press its buttons, read its screen, and observe how it reacts to the outside world — instead of settling for a headless approximation that structurally cannot show the behaviour under test.
The principle worth internalising
claude -p is a different product surface, not a cheaper layer of the same one. Whole classes of behaviour exist only in the interactive TUI:
| Only in interactive | Why headless can't show it |
|---|---|
Inbound mesh <channel> tags |
CC drops them at the surfacing layer in -p; the wire delivers, the context never sees it |
| Trust / permission / channels dialogs | No TTY, no prompt |
| Statusline, glyphs, live indicators | Nothing renders |
| Session-start rituals and their hooks | Different entrypoint, different lifecycle |
CC names the distinction itself: CLAUDE_CODE_ENTRYPOINT is sdk-cli for -p and cli for interactive. Testing an interactive-only behaviour headless and reporting a result is the unearned green in its most convincing costume — the check ran, it passed, and it never touched the thing at risk.
When to Use
- Mesh receive-path work — does a session actually surface a peer's message as a tag? Only an interactive session can answer.
- Dialog flows — folder trust, the channels warning, permission prompts. Reaching them at all needs a TTY.
- Statusline and glyph work — verifying what a human would see.
- Launch-wrapper verification — does
claudem/-mproduce the session shape it promises? - Anything whose evidence is "what appeared on screen" rather than "what a tool returned".
Boundaries
- Not for headless work. If
-pcan genuinely exhibit the behaviour, use-p— it is faster and needs no teardown. - Not for isolation testing. A blank-slate session (no CLAUDE.md, no skills, no plugins) is
ardoise. The two compose:hublotcan drive an interactiveardoisesession, which is the only way to test a fresh user's onboarding flow unattended. - Not a substitute for a unit test. Drive the TUI to confirm the seam; assert the logic offline.
- Linux-first. Built on tmux and
capture-pane.
Workflow
Resolve the script the way sibling skills do:
SCRIPT="${CLAUDE_PLUGIN_ROOT}/scripts/hublot.sh"
[ -x "$SCRIPT" ] || SCRIPT=$(find ~/.claude/plugins/cache -path "*/trousse/*/scripts/hublot.sh" 2>/dev/null | sort -rV | head -1)
1. Choose a cwd that is already trusted
A new folder raises CC's folder-trust dialog, and answering it writes a durable entry into the user's ~/.claude.json — a config change as a side effect of a test. Check first:
python3 -c "
import json; d=json.load(open('$HOME/.claude.json'))
print([k for k,v in d.get('projects',{}).items() if v.get('hasTrustDialogAccepted')])"
Success: you have a trusted directory that is not the repo under test if that repo has its own MCP config (a project .mcp.json server plus a plugin server means two servers and a muddied result).
2. Start, scrubbing whatever the test must not inherit
$SCRIPT start probe --cwd ~/repos/spm1001/some-trusted-repo \
--unset CLAUDE_CODE_USE_VERTEX --unset ANTHROPIC_MODEL -- claudem
--unset scrubs inside the session's shell. Billing env is the usual case: a Vertex-billed parent makes every child Vertex, and some behaviour (channels) is unavailable there.
Choose the wallet on purpose — the command after -- decides who pays, and a probe is not free. Bare claude bills the seat the human is working on; claudev bills Vertex. A driven session runs a whole session's worth of turns (a session-start ritual alone can be several minutes of a large model), so a handful of probes is a real dent. Measured 2026-09-01: two hublot sessions, one of them running a full orientation over a large repo, took the operator's Teams seat to its 5-hour limit — which then blocked the very fixture the probes were for, for eighty minutes. Ask which wallet can afford this before you type the command, not after the limit lands.
Success: start reports the tmux session name.
3. Wait for a pattern, never sleep and hope
$SCRIPT wait probe 'I am using this for local development' 60
$SCRIPT enter probe # accept the dialog
$SCRIPT wait probe 'Channels .experimental.' 60
wait polls and exits non-zero on timeout, printing the pane so you can see where it stalled. A fixed sleep is both flaky and slow.
Success: each wait returns matched:.
4. Exercise and observe
Do the thing from outside — send a mesh message, touch a file, trigger a hook — then read the screen:
$SCRIPT read probe -n 20 # visible pane
$SCRIPT read probe --all # include scrollback
Success: the pane shows the behaviour, and you have a positive control (something that should appear, appearing) so an absence is a finding rather than a dead instrument.
5. Stop cleanly
$SCRIPT stop probe
Clears the composer (C-u), then sends /exit before killing tmux. The clear matters because ghost/suggestion text can be sitting in the composer after an action-capable turn, and /exit appended to it would submit the lot as a prompt nobody wrote. The clean exit matters for mesh work: it deregisters, while a kill leaves a roster ghost for the 60–120s expiry window that will confuse the next test.
read the pane before you stop it — a driven session is a real session, and a human may have joined it. Yours is not the only keyboard: the tmux session is attachable, and an operator glancing at their windows can find a live Claude that asked a sensible question and answer it. On 2026-09-01 a probe session ended its /open by asking which direction to take, the operator typed a reply into it, and the teardown three minutes later cleared the composer and exited — the reply and the thread went with it. Nothing was lost from disk that time; that was luck, not the design. So before stop: read the pane, and if there is text you did not send or a turn you did not prompt, leave it alone and say so rather than tearing it down.
Driving TUI menus (/config, /hooks)
The key verb sends named tmux keys — Down, Up, Escape, PPage, NPage, Tab, Enter, C-c — one argument per press, repeats allowed. Menus need key; prose needs keys (which sends literally, so text that happens to be a key name is typed, not pressed).
$SCRIPT keys probe "/hooks" # open the menu (keys types, settles ~1s, submits)
$SCRIPT key probe Down Down Enter # walk to an event and open it
$SCRIPT read probe -n 30 # read the matcher list
$SCRIPT key probe Escape # back out without changing anything
/hooksanswers settings-scope questions no file read can: each matcher line carries its source —[User],[Local],[Plugin]— so "which file contributed this hook?" reads straight off the screen./config: type-to-filter works at the search box, and/re-opens search from the list.- Prefer
Escapeto back out when the goal is observation — it leaves settings uncommitted.
Common Mistakes
| Mistake | Symptom | Better |
|---|---|---|
env -u FOO claudem |
env: 'claudem': No such file or directory |
env execs a binary; a shell function is not one. Use --unset, which scrubs inside the shell. |
bash -lc 'claudem' |
claudem: command not found |
bashrc returns early when non-interactive, so wrapper functions are never defined. start uses a genuine interactive shell for exactly this reason. |
| Launching in an untrusted cwd | An unexpected trust dialog, then a new entry in the user's config | Pick an already-trusted folder (step 1). |
sleep 30; capture-pane |
Flaky and slow at the same time | wait with a pattern. |
| Reading the pane once and concluding | An absence that is really a timing artefact | Include a positive control; assert the thing that should appear does. |
| Killing the session outright | Roster ghosts, dirty state, missed teardown hooks | stop (it sends /exit first). |
stop without reading the pane |
A human joined the driven session; teardown clears their reply and kills the thread | read first. Text you didn't send means someone else is in there. |
Reaching for bare claude out of habit |
The probe bills whichever seat the human is working on, and can exhaust its limit | Pick the wallet: -- claudev for probes that don't need the seat. |
Testing in a repo with its own .mcp.json |
Two MCP servers, one agent id, confusing results | Drive from a neutral trusted folder. |
Blind enter after an action-capable turn |
Ghost/suggestion text in the composer submits as a prompt nobody wrote | read the pane first; key NAME C-u clears anything sitting there. stop does this automatically before /exit. |
Quick Reference
$SCRIPT start NAME [--cwd DIR] [--unset VAR]... -- CMD... # launch under tmux
$SCRIPT wait NAME REGEX [TIMEOUT_S] # block until it appears
$SCRIPT keys NAME "text" [--no-enter] # type + submit (settles ~1s before Enter)
$SCRIPT key NAME KEY [KEY...] # named keys: Down Escape PPage Tab ...
$SCRIPT enter NAME # bare Enter (confirm a dialog)
$SCRIPT read NAME [-n LINES] [--all] # what is on screen
$SCRIPT stop NAME [--no-exit] # clear composer, /exit, then kill
$SCRIPT list # live hublot sessions
Pane size: HUBLOT_COLS / HUBLOT_ROWS (default 200×50).
Integration
- ardoise — isolates what a session knows; hublot drives what a session does. Compose them to test a fresh user's onboarding, dialogs included, without a human.
- peer-message receive paths — whether a session actually surfaces an inbound message is interactive-only, so hublot is the only honest way to verify it. Ring the pane with
sonner <repo> "…"and read the screen for the peer-message framing. (This bullet named sonnette until 2026-08-24, when it was delisted from the suite — son-pilalu. Its/tmp/conductor-bridge/{agentId}/capabilityfile went with it; sonner has no send-only/bidirectional split to check, but a provider-billed session registers with no inbox socket at all, which is the equivalent thing to catch.) - bon — when a test produces a finding, file it; when it produces a technique, extend this skill.
Dependencies
tmux. Everything else is the session under test.