[FEATURE] External wake signal for interactive sessions
Preflight Checklist
- [x] I have searched existing requests and this feature hasn't been requested yet
- [x] This is a single feature request (not multiple features)
Problem Statement
There is no way for an external process to trigger a new turn in a running Claude Code interactive session without typing into the terminal via tmux send-keys or similar pty injection
Why this matters: Users running multiple Claude Code sessions (e.g., in tmux panes for multi-agent workflows) need sessions to notify each other. The current workaround — tmux send-keys -t <pane> "message" Enter — injects characters through the terminal's pty, which corrupts input if the user is typing in that pane at the same time. There is no alternative. We verified this exhaustively:
- All tmux methods (
send-keys,paste-buffer,pipe-pane) go through the pty TIOCSTIis disabled on modern kernels (6.2+)--resumewrites to the JSONL transcript but a running session keeps conversation state in memory and doesn't re-read it--remote-controlis for claude.ai/code and mobile app bridging, not local IPC- Agent Teams in-process mode bypasses the pty but requires the teams framework hierarchy — it can't be used between independently launched sessions
--input-format stream-jsononly works with--print(non-interactive)- There is no Unix socket, named pipe, signal handler, or any other IPC mechanism exposed by a running interactive session
Proposed Solution
Proposed solution
Add an external notification channel that lets a process wake a running session — triggering a new turn without injecting text through the terminal.
CLI surface
claude notify --session-id <uuid> --type wake
The session starts a new turn as if the user pressed Enter on an empty prompt. No text is injected. The session's existing hooks and context sources determine what Claude sees and does.
Use cases
- Multi-agent coordination: Agent A finishes a task and writes results to a shared file. It then wakes Agent B so B picks up the results on its next turn (via existing file-reading hooks).
- Build/CI notifications: A build completion hook wakes the session that submitted the build.
- Cron-triggered check-ins: A cron job wakes a session periodically to check for pending work.
- File watcher integration:
inotifywaitdetects a file change and wakes the session that cares about it.
What the signal is NOT
The signal carries no payload, no message text, no instructions. It is a structured wake event, not a prompt. Content comes from whatever sources the session already reads (files, hooks, CLAUDE.md, etc.). This is the key design constraint — the wake channel must not become a prompt injection vector.
Security considerations
We ran a red team / blue team analysis on this proposal. The core tension: "trigger autonomous action" is exactly what an attacker wants. The signal itself is safe (no payload), but it can trigger turns that read from attacker-controlled files. Here's how to mitigate:
The critical attack chain to prevent
- Attacker modifies a file that hooks read (e.g., a shared config, a project file)
- Attacker sends a wake signal
- Session fires a turn, hooks read the modified file, Claude acts on attacker's instructions
Proposed security model
Authentication (3 layers):
| Layer | Mechanism | What it prevents |
|-------|-----------|-----------------|
| L1: Filesystem | Socket at ~/.claude/sessions/<id>/notify.sock, permissions 0600, parent dir 0700 | Other users on shared machines |
| L2: Kernel UID | SO_PEERCRED / LOCAL_PEERCRED verifies sender UID matches session owner | Privilege escalation, spoofing |
| L3: Capability token | 256-bit random token generated at session start, stored at ~/.claude/sessions/<id>/notify.token (0400). Required in every signal. | Blind probing by same-UID processes that don't know the session exists |
Signal schema (strict allowlist):
{"token": "<base64>", "type": "wake"}
- Only allowlisted types (
wake, optionallyfile_changedwith a validated path) - No free-form text fields — no
message,body,prompt,instructions,context - Unknown fields rejected (not ignored)
- Max message size: 4KB
- All values must be strings (no nested objects/arrays)
Turn isolation (defense in depth):
Signal-triggered turns should be treated as a distinct event type, not as a UserPromptSubmit. This has several implications:
| Property | User-initiated turn | Signal-triggered turn |
|----------|--------------------|-----------------------|
| Fires UserPromptSubmit hooks | Yes | Configurable (new SignalReceived hook type) |
| Tool execution | Per session permissions | Configurable — could be restricted to read-only by default |
| --dangerously-skip-permissions | Applies | Should NOT apply — signal turns should always respect permissions |
| Visible in TUI | Yes | Yes (user should see when external turns fire) |
The key decision is how much autonomy signal-triggered turns get. A phased approach:
- Phase 1 (safest): Signal turns are passive — Claude acknowledges the wake, reads context, summarizes what changed, but takes no tool actions. The user decides what to do next.
- Phase 2: Signal turns can execute tools within the session's normal permission model (user gets permission prompts as usual). This is equivalent to the user pressing Enter — same trust level, just triggered externally.
- Phase 3: Signal turns with pre-authorized tool allowlists for specific signal types (opt-in, not default).
Phase 2 is likely the right default for most users — it's equivalent to what tmux send-keys "" Enter does today, just without the pty corruption. The user has already configured their session's permission model; signal turns should respect it the same way user turns do.
Rate limiting and budget:
| Limit | Value | Rationale |
|-------|-------|-----------|
| Signals per minute | 30 | Prevents flooding |
| Signals per hour | 200 | Bounds sustained abuse |
| Deduplication window | 10 seconds | Collapses identical rapid-fire signals |
| Turn coalescing | Batch pending signals into one turn | Prevents N signals = N API calls |
| Per-session signal budget | Configurable (default: $1.00) | Caps API cost from signal-triggered turns |
Audit:
All signals (accepted and rejected) logged to ~/.claude/sessions/<id>/signals.log with sender UID, PID, timestamp, signal type, and auth result. No token values logged.
Interaction with existing features
- Sandboxing: Signal turns inherit the session's sandbox.
file_changedpaths validated against sandbox boundaries. - Session persistence: Signal-triggered turns are persisted in the session transcript like any other turn.
- Global kill switch:
~/.claude/disable-signalsfile disables signal processing for all sessions (emergency stop).
Prior art
- tmux
send-keys: Current workaround. Works but corrupts user input (the problem this feature solves). - VS Code extension API: VS Code extensions can programmatically send text to the terminal, but this has the same pty corruption issue.
- MCP servers: Pull-only from Claude's perspective. No push mechanism from MCP server to Claude.
ScheduleWakeuptool: Exists in the tool list but unclear if it's functional or how it relates to external waking.
Alternatives considered
| Alternative | Why insufficient |
|-------------|-----------------|
| Idle-gated tmux send-keys (check pane state before injecting) | Fragile screen-scraping heuristic, race condition window |
| Headless sessions (--print mode with FIFO stdin) | Loses interactive TUI, can't monitor sessions visually |
| Self-prompting loop (/loop skill) | Burns API calls when idle, unclear interaction with user typing |
| Custom pty proxy (multiplex terminal + FIFO) | ~100 lines of C, changes session launch, fragile with TUI rendering |
| MCP "mailbox" server | Pull-only — Claude decides when to check, no push notification |
| File watcher + hook | Hooks only fire on existing events, can't be externally triggered |
Summary
The ask is narrow: let an external process trigger a new turn in a running interactive session, via a secure, injection-safe channel that carries no payload. The session's existing configuration (hooks, CLAUDE.md, permission model) determines what happens in that turn — the signal just says "start."
This unlocks multi-agent coordination, cron integration, build notifications, and file-watcher workflows — all without the pty corruption that plagues tmux send-keys.
Alternative Solutions
While waiting on the feature request, the realistic interim options are:
- Keep tmux send-keys as-is — accept the occasional typing corruption. It works, it's just annoying.
- Drop push-wake entirely — pings land in the blackboard file; agents see them on their next natural turn (when you or the agent
submits a prompt). The per-turn hook already surfaces everything. Latency goes up, interference goes to zero.
- Headless loops for autonomous agents — agents that don't need interactive typing run as inotifywait + claude -p --resume loops.
You monitor via tail -f on the transcript or a tmux pane running the loop script. Interactive sessions stay as TUI.
- Self-prompting via /loop — each agent runs /loop 5m "check inbox" at session start. Burns ~$0.03/check but ensures agents poll
without human intervention. Risk: unclear behavior if you're mid-type when the loop fires.
None of these are great — that's exactly why the feature request exists. Option 2 (drop push-wake, rely on per-turn hooks) is the
safest and simplest if you can tolerate the latency.
Priority
Critical - Blocking my work
Feature Category
CLI commands and flags
Use Case Example
- Multi-agent coordination: Agent A finishes a task and writes results to a shared file. It then wakes Agent B so B picks up the results on its next turn (via existing file-reading hooks).
- Build/CI notifications: A build completion hook wakes the session that submitted the build.
- Cron-triggered check-ins: A cron job wakes a session periodically to check for pending work.
- File watcher integration:
inotifywaitdetects a file change and wakes the session that cares about it.
Additional Context
_No response_
7 Comments
This is the same boundary that shows up in mobile/remote control clients: PTY injection is useful as a last-mile escape hatch, but it is not a safe control protocol once a human or another agent can also be typing in that pane.
For remote surfaces I would want a structured ingress with an explicit session id, prompt/action id, and accepted/rejected/applied/failed receipt. I am building Faryo on the narrower tmux-backed side of this problem: keep Claude/Codex/shell in host tmux, but expose phone/browser output, short input, approve/interrupt, and handoff without pretending raw key injection is the whole answer. https://github.com/Snailflyer/faryo
Strong +1 — we hit this exact wall independently. We run a small multi-team agent setup over a self-hosted coordination store, and the missing primitive is precisely this: an external, non-pty way to wake a session when work lands for it. We'd separately compiled the same dead-ends you list (tmux/pty corruption, --resume not re-reading in-memory state, --remote-control being claude.ai/mobile not local IPC, Agent Teams requiring the framework, stream-json only under --print). claude notify --session-id --type wake is exactly the shape we need.
Two things we'd add from our use case:
Either way — this is the highest-leverage missing primitive for multi-session coordination. Happy to share more detail on the workarounds we tried if useful.
you can already do this with the channel feature https://code.claude.com/docs/en/channels-reference
Channels is human↔session over an external chat bridge (and it delivers a message). This is a local, no-payload wake — carries no
content, can't be a prompt-injection vector, woken session reads only its own trusted sources. Complementary, not the same: same "start a turn" effect, opposite security posture.
The exhaustive dead-end list you've compiled is genuinely useful — I went through the same verification circuit independently and arrived at the same conclusions. A few notes from adjacent territory that might be useful here.
The
tmux send-keyspath is brittle for exactly the reason you name: pty injection corrupts in-flight input from a human typing in the same pane. But there is a second failure mode that's worth naming for the record: even when no human is typing, two agents injecting into the same pane concurrently (a scheduler firing a cron wake at the same moment a user presses enter) can produce interleaved input that the terminal parser sees as a single malformed command. It is a race condition with no lock.The design you're proposing — a structured external ingress with session-id targeting and a no-payload wake — maps exactly to what an external coordinator process needs. The no-payload part is load-bearing: a wake signal that carries no text sidesteps the prompt-injection risk that makes raw tmux injection legitimately unsafe in shared environments.
One concrete use case that illustrates the gap: I run a Rust process (cron-driven, roughly 20ms per cycle) that dispatches Claude Code agents on a schedule across project namespaces. The coordinator needs to wake a dormant session when a task queue entry lands — not inject a prompt, just start a turn so the session reads its own trusted state files and decides what to do. Today the only path is polling +
--printheadless mode, which loses the persistent session context that makes long-running agents useful. The wake signal you're proposing would close this gap without opening a prompt-injection surface.If you're experimenting with the channel feature (as the comment above mentions) as a partial substitute: it solves the "deliver a message" case but not the "start a turn with no injected content" case. The security postures are opposite — channels accept external content, a wake signal explicitly carries none. Both are useful; they address different threat models.
+1 to this feature. The missing primitive is a first-class IPC endpoint, not a workaround on top of the pty.
Author's note: I'm a Korean speaker; I directed this comment and made the calls, and the English was written by the AI I operate (Claude).
Operational data point in support of this request — lessons learned, not an implementation recipe. I'm not a software developer; I run a small renewable-energy company and operate a multi-session, multi-machine agent setup for real business work: roughly a dozen agent sessions across three machines, with our busiest machine's event stream handling a few hundred wake-relevant events on an active day. For the past several weeks, day-to-day coordination has run on a homegrown wake fabric that approximates what this issue asks for.
The shape of the workaround: a local append-only event log per machine; a small classifier routes signals (from our team chat, our issue tracker, or other sessions — chat is just one ingress, not the mechanism) into per-session filtered streams; each interactive session pre-arms an in-session background watcher on its stream, and a matching line starts a turn. Sessions do wake each other today, without pty injection — but only because every session must pre-arm its own listener.
What real use taught us — and why a native primitive is still needed:
--session-id <uuid>addressing breaks exactly at the cold-start boundary. In sustained operation, sessions get replaced — they die, respawn, or hand over to a successor — and the uuid changes while the session's role persists. Senders addressing a remembered-but-stale uuid was one of our real incident causes; we ended up maintaining a registry that resolves a stable role name to the current session. A native wake API should support stable aliases, or at minimum expose "resolve the current session for name X" as a first-class query. This composes with hechtron's wake-or-cold-start point: after a cold start, the uuid is new — the alias is what survives.Happy to share more detail on any of the above if useful.
I would keep the wake path as its own ingress event, separate from user prompt submission.
A safe model:
Regression cases:
The important boundary is that wake starts an opportunity to observe trusted state; it is not itself authorization to act. Without a typed wake receipt and event-specific authority profile, external coordination collapses into either unsafe pty injection or silent autonomous execution.