bash sleep 300 pattern that locks the screen for 5 minutes, the agent emits a single @park tool call that:
- Snapshots the loop state (history, counters, mode) to disk.
- Frees the terminal — you can chat, list jobs, open another
/coder. - Schedules the resume on the durable scheduler (survives crash/restart).
- Auto-resumes by itself once the timer/poll completes, without you pressing Enter.
Available since chatcli 1.111.x (PR #879). Works in
/coder, /agent and /run modes. Auto-resume uses TIOCSTI on Unix and WriteConsoleInputW on Windows; transparent fallback when the OS restricts the injection.Flow overview
Why this is needed
Before park, waiting inside a/coder meant:
- Terminal blocked for the full 300 s.
- Each
bashcall burns a turn of the agent’s turn budget (default 100). - CLI crash = lose the sleep and the state.
- No audit trail — only in shell history.
@park:
- Terminal freed immediately; you keep using the CLI.
- Park takes a single turn regardless of duration (10 s or 14 days).
- Crash-safe — disk snapshot + scheduler WAL replay on boot.
- Full audit via
/jobs logsand/parked.
Four modes of @park
- delay
- until
- for_url
- for_cmd
Fixed timer. Single-shot. Ideal for “wait before checking again”.
success_when matchers
Free-form DSL. Empty assumes “default success” (HTTP 2xx or exit 0).
Multiple matchers in a single spec are not supported; combine using a custom
body matches: if you need logic.
Management commands
- /parked
- /resume
- /cancel-park
- /park-note
Lists all on-disk parks with cross-checked scheduler job status.Subcommands:
Mid-park directives
While an agent is parked the terminal is yours: plain text goes to normal chat, every command works, and you can even run another/coder task in parallel. To talk to the parked agent, use the explicit channels:
This makes parked loops composable: a second agent investigating an incident can
@park list, spot the monitoring loop parked for 10 minutes, and @park note it — “the deploy finished, validate and stop monitoring” or “switch your cycle to 5m”. The parked agent applies cadence changes itself on its next cycle, so scheduler ownership never leaves the loop that created the park.
At resume time the agent receives the directives as a block:
Auto-resume — how the terminal “wakes up by itself”
This is the part that distinguishes park from a plain scheduled task: when the wait completes, the agent returns to foreground without you doing anything.Why TIOCSTI
TIOCSTI is a POSIX ioctl that injects bytes into the TTY’s input buffer as if the user had typed them. It works with any application reading stdin from the controlling tty — no need to modify go-prompt.
Why two bursts (body + 15 ms + \r)
go-prompt v0.2.6 usesbytes.Equal to classify keys (input.go:24). A multi-byte buffer like /resume abc\r doesn’t match any sequence in the ASCII table and falls into the default branch which inserts as text — including the trailing \r, which becomes literal and never submits. Solution: split.
- Command body in one burst (multi-byte → text insertion).
- 15 ms pause (above
readBuffer’s 10 ms poll cycle). - Lone
\rin a second burst (single byte → matches ControlM → submits).
Windows uses WriteConsoleInputW
No TIOCSTI on Windows — kernel32.dll exposesWriteConsoleInputW which accepts structured INPUT_RECORD events. Each char becomes a key-down/key-up pair; the trailing Enter uses VirtualKeyCode=VK_RETURN so go-prompt’s reader classifies it as a native Enter.
Platform support matrix
- Linux
- macOS
- Windows
- BSDs
TIOCSTI gated by Trade-off: re-enables a feature distros disabled because of CVE-2017-5226 (sandbox escape via injection). Safe in personal dev environments; on shared servers, prefer the fallback.
/proc/sys/dev/tty/legacy_tiocsti:To re-enable (root):
Fallback when TIOCSTI/WriteConsoleInput is unavailable
When the injection is rejected, the🔔 park ready banner still shows up and the token enters the pendingResumeQueue. You need to type any character + Enter at the prompt — the executor consumes the queue before processing your input. Equivalent UX with one extra keystroke.
The prompt prefix shows [🅿️ resume ready: N] ❯ while a resume is pending, so it’s hard to forget.
Real-world examples
GitHub Actions CI
/coder to refactor tests in parallel. ~15 minutes later:
Slow terraform apply
terraform plan -detailed-exitcode returns 0 on no diff, 2 on diff present, 1 on error. Here we wait for 0 (convergence). To wait for “diff applied”, swap to success_when:exit=2.
Off-peak deploy window
Post-rollout health check
Security model
Approving @park = approving the polling
When the agent emits@park for_cmd cmd="echo done", /coder shows the security check with the full args, including the embedded cmd:
[y], you are pre-authorizing the polling shell to run that specific cmd you just saw. ChatCLI propagates DangerousConfirmed=true on the scheduler job, so the poll’s fire-time recheck does not stumble on ShellPolicyAsk (no human at the keyboard at fire time to approve again).
Snapshots are 0o600
~/.chatcli/parked/<token>.json contains the park’s full chat history. Files are created with 0o600 (owner-only) and the directory with 0o700. Snapshots never leak to other users on the host.
Token cannot path-traverse
Tokens are generated withcrypto/rand (16 bytes hex = 32 chars) and validated against regex [a-zA-Z0-9._-]{8,128}. There is no way for /resume ../etc/passwd to escape the directory.
Environment variables
Most behavior is governed by the underlying scheduler — see Scheduler env vars.
Internals — for hackers
Snapshot format
JSON serialized withjson.MarshalIndent. Versioned schema (SchemaVersion = 1). Main fields:
pending_tool_call_id is the Anthropic native tool_use ID — preserved to reconstruct the tool_use/tool_result pairing on resume (otherwise the next API request rejects with unmatched tool_call).
Scheduler action types
Park introduces 2 action types:park_poll self-reschedules every interval until match or deadline elapses. Crash-safe via WAL replay — an interrupted iteration resumes on boot.
/resume idempotency
Auto-resume injects/resume <token>\r via TIOCSTI. But the executor already ran the resume on the first line (drainPendingResumes), so when the /resume <token> command reaches the handler the snapshot was already deleted.
Solution: markRecentlyResumed(token) on drain (TTL 30 s) and wasRecentlyResumed(token) in handleResumeCommand → silent no-op. Genuinely invalid tokens (user typos) still surface as errors because the TTL is short.
Troubleshooting
park: no snapshot matching '<token>'
park: no snapshot matching '<token>'
You’re using the job ID (second column of
/parked) instead of the token (first column). Tokens have 8 visible chars in /parked; job IDs are scheduler-internal.Always copy from the first column of /parked or use auto-complete (Tab).Park stuck in (failed) on /parked
Park stuck in (failed) on /parked
Check
/jobs show <job_id>. Common causes:echo done(or any cmd) classified Ask without DangerousConfirmed: should be propagated automatically; report as a bug.- Persistent HTTP 5xx in for_url: each poll fails, scheduler may mark the job as failed after N retries. Increase
intervalordeadline. - Denylisted shell command: a
Denyrule in the coder policy always wins, even with approval. See/config security rules.
Snapshots accumulating in ~/.chatcli/parked/
Snapshots accumulating in ~/.chatcli/parked/
Use
/parked prune to remove snapshots whose job is terminal (completed/failed/cancelled/timed_out). On long-running systems, consider periodic /parked gc 24h.How do I tell which controlling TTY receives the injection?
How do I tell which controlling TTY receives the injection?
/dev/pts/N (Linux) or /dev/ttysNNN (macOS). That’s the fd injectTTYLine opens via /dev/tty.Quick reference
@coder exec, see Coder Security.