[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_
This issue has 7 comments on GitHub. Read the full discussion on GitHub ↗