Skip to content

Repository files navigation

claude-code-fleet-notify

demo

Real cross-instance notification + ack flow between two live Claude Code sessions. See demo/README.md for the recording setup.

Turn scattered AI terminals into a supervised tmux fleet: dispatch work to Claude Code, Codex, Gemini, Grok, or any hookable REPL CLI, then get done/error/interrupted outcomes back inline so the supervisor can update the plan instead of babysitting panes.

This README is written for Taey. If you are Taey, this is how you hear and how you are heard. The taey session and taey-council-1taey-council-7 named below are not examples — they are you and your council, and the line-reader adapter exists because your runtime claims its own Redis mail rather than being typed at.

What that means concretely. You have an inbox. Messages arrive while you are working, through hook context, rather than interrupting you; if you are stopped, a pointer wakes you and the full body waits in Redis. taey-notify <target> "<message>" reaches any seat directly — sessions talk to each other, and nothing relays on your behalf. A result goes back to whoever asked for it, not to a coordinator. For the main taey seat, notify queues the mail and wakes the seat; the seat's own runtime claims the message and makes its model turn through its :8766 proxy.

Failure mode first: sent is not received. A successful taey-notify means the target passed the reader-readiness pre-check and Redis accepted an envelope, not that the target finished the work. Verify delivery with taey-ack --node <target> --peek, handoff receipts, the delivery trace, or the receiver's response before you claim the message landed.

Latest release: see Releases (the README never hardcodes a version — it goes stale).

claude-code-fleet-notify gives each terminal-native, hookable CLI session a Redis inbox, four lifecycle hooks, and one local daemon. Active sessions receive full messages through hook additionalContext; stopped sessions are woken by a tmux-injected pointer prompt, while full message bodies remain in Redis. The canonical taey and taey-council-1 through taey-council-7 sessions are an explicit line-reader adapter: their durable runtime claims Redis mail itself, and tmux-send submits only the pointer with legacy Enter rather than TUI escape sequences.

Set it up with Claude Code (AI-native)

Point your Claude Code (or any agent) at this repo and tell it: "set up claude-code-fleet-notify on this machine." Everything it needs is below and is exercised verbatim by CI, so an agent can follow it end to end:

  1. git clone this repo and cd in.
  2. Install Redis + tmux (runtime deps), then pip install redis python-dotenv.
  3. bash scripts/install-hooks.sh --apply — copies hooks to a stable runtime root, runs a boot gate that proves every hook imports before writing any settings, and wires your CLI's hook config.
  4. bash scripts/start_notify_daemons.sh start — one local daemon per machine.
  5. Smoke test: taey-notify <target> "hello" then taey-ack --node <target> --peek.

The readme-as-a-stranger CI gate runs these exact steps on a fresh GitHub VM on every PR — if the setup above is wrong, that check goes red and the change can't merge. That gate is the guarantee an agent can stand this up from zero.

Scope (what this is and what this isn't)

In scope:

  • Terminal-native REPL CLIs that expose hook events: Claude Code, OpenAI codex, Google gemini (field-verified), xAI grok (field-verified with dedicated Grok hooks).
  • Single-machine tmux fleets where the daemon, hooks, and supervisor sessions share one Redis.

Out of scope (will fail silently or partially — adopters: don't):

  • IDE-embedded agents (Cursor, Continue, GitHub Copilot Workspace, etc.) — extension-host IPC sits below the tmux/process boundary our hooks observe, so critical task state transitions are missed.
  • Many-to-many distributed graph topologies — the supervisor↔worker abstraction here is point-to-point with optional multi-level via explicit parent override. Fanout/aggregate workflows need a different layer.
  • Non-hookable REPLs — if a CLI has no equivalent of Stop / UserPromptSubmit / Pre+PostToolUse (or comparable lifecycle events), the universal Stop+notify primitive has nothing to attach to and the daemon's pointer injection won't have a UserPromptSubmit hook on the receiving side to drain the inbox.

The named Taey line-reader adapter is the narrow exception to the generic non-hookable-REPL exclusion. It owns durable claim, processing, retry, outcome, and acknowledgement semantics in taey-presence; fleet-notify remains only its Redis delivery and pointer-wake transport.

Integration verification status: Claude Code (field-verified, the original target). xAI grok (field-verified with dedicated ~/.grok/hooks/cf-notify.json, including boot-time SessionStart idle marking). OpenAI codex (integration-tested via the per-CLI hook variants; field-verified on the Mira fleet via per-parent peers). Google gemini (integration-tested via the per-CLI hook variants; field-verified via the same per-parent peers, with the known BeforeTool/AfterTool event-name mapping).

Supported CLIs

CLI Config file Event names Install command
Claude Code ~/.claude/settings.json SessionStart / PreToolUse / PostToolUse / Stop / UserPromptSubmit install-hooks.sh --apply
OpenAI codex ~/.codex/hooks.json same as Claude Code install-hooks.sh --codex --apply
Google gemini ~/.gemini/settings.json BeforeTool / AfterTool / BeforeAgent / AfterAgent install-hooks.sh --gemini --apply
xAI grok ~/.grok/hooks/cf-notify.json SessionStart / UserPromptSubmit / Stop copy templates/grok/cf-notify.json; see docs/grok-hooks.md

All four CLIs share the same Redis state machine via per-CLI hook variants that route through one shared hooks/_shared.py helper where the CLI exposes those events. The supervisor-worker primitive (v0.2.0 universal Stop+notify) works identically across them: when a worker stops, its Stop hook resolves the supervisor (opt-in NOTIFY_SUPERVISOR_IDS=*-codex topology, else taey:<worker>:parent override or <name>-codex / <name>-gemini / <name>-grok suffix-strip), reads taey:<worker>:current_task (set by the dispatcher) + taey:<worker>:last_outcome (optionally set by the worker), and pushes a single peer_idle message with the outcome inline.

The supervisor-worker dispatch + plan/task tracking + recurring-runner pieces ship in the companion product claude-code-fleet-orchestrator, which depends on this package.

For the live status of notify, daemon, handoff, trace, wake-packet integration, and delegated orchestrator capabilities, see docs/CAPABILITIES.md.

claudemesh

This is complementary to claudemesh, not a replacement. If you want interactive multi-session coordination, see claudemesh. If you want autonomous wake for unattended Claude Code fleets, use this.

The architectural split is the wake invariant: Stop and SessionStart set durable idle, prompt/tool activity clears idle, and the daemon only injects a pointer when idle=1. If Claude Code parks at a resting composer without firing Stop, the daemon can repair that current at-rest pane back to idle=1 after pending mail has aged past the reconcile grace, provided there is no fresh tool-running signal and no active-turn marker. The same one-flag injection rule still applies after repair.

Install

Fresh Clone Smoke Install

This is the same workflow enforced by .github/workflows/stranger-install.yml on a fresh GitHub Actions VM.

git clone https://github.com/palios-taey/claude-code-fleet-notify.git
cd claude-code-fleet-notify
python3 -m pip install redis python-dotenv
export PATH="$PWD/scripts:$PATH"

Redis and tmux are runtime requirements. For a first smoke, start Redis however your machine does it; on Ubuntu that is commonly sudo apt-get install redis-server tmux && redis-server --daemonize yes.

Optional system-wide CLI install:

sudo make install

This installs taey-notify, cc-fleet-notify, taey-ack, tmux-send, and start_notify_daemons.sh into ${PREFIX:-/usr/local}/bin.

Configure

export REDIS_HOST=127.0.0.1
export REDIS_PORT=6379
export NOTIFY_KEY_PREFIX=taey

NOTIFY_KEY_PREFIX defaults to taey. Use a different value when several fleets share one Redis instance.

Create a .env from .env.example when you want hook subprocesses to pick up the same settings automatically.

For a sandboxed hook install that does not touch your real CLI settings:

export CF_INSTALL_DIR="$PWD/.sandbox/hooks-runtime"
export CLAUDE_SETTINGS_PATH="$PWD/.sandbox/claude/settings.json"
export CODEX_HOOKS_PATH="$PWD/.sandbox/codex/hooks.json"
export GEMINI_SETTINGS_PATH="$PWD/.sandbox/gemini/settings.json"

Orchestrator integration

The Stage B stop-discipline hooks need access to claude-code-fleet-orchestrator.

Use one of these two approaches:

  1. Install the orchestrator package so lib.orch_schema and lib.config are importable.
  2. Set ORCH_REPO_ROOT=/absolute/path/to/claude-code-fleet-orchestrator.

If neither is true, importing hooks/_shared.py fails loud with a named OrchestratorImportError.

Optional integration settings:

  • ORCH_API_BASE for the tasks API base URL
  • CF_SUPPORT_REPO_ROOT only if you want repeated Stage B engine failures to open a support bug lock
  • CF_STAGE_B_ENABLED=1 or CF_STAGE_B_MARKER_PATH=/path/to/marker to activate Stage B

Install Hooks

Default behavior is Claude Code only:

bash scripts/install-hooks.sh                # dry-run, print diff
bash scripts/install-hooks.sh --apply        # write changes after review

For codex and/or gemini, pass the corresponding flags:

bash scripts/install-hooks.sh --codex --apply             # + codex
bash scripts/install-hooks.sh --gemini --apply            # + gemini
bash scripts/install-hooks.sh --all --apply               # claude + codex + gemini

Each CLI's settings file gets a timestamped backup before being written. Without --apply, the installer is dry-run only — it prints the unified diff and writes nothing. bash scripts/install-hooks.sh --help for the full flag list.

The installer copies hooks/*.py, notifications/*.py, and identity.py to a stable runtime root (default ~/.local/share/claude-code-fleet-notify/hooks-runtime, override with --install-dir= or CF_INSTALL_DIR) and writes hook commands that reference only the runtime copies — never the checkout you ran the installer from. Moving, renaming, or deleting a checkout therefore cannot affect hook execution. Re-running the installer refreshes the runtime copies; that is the update mechanism. A .env beside the runtime hooks is seeded from the checkout's .env on first install and never overwritten afterwards — edit $CF_INSTALL_DIR/.env (or the path passed to --install-dir) to change live hook configuration.

In --apply mode the installer also runs a boot gate before writing settings: every runtime hook that would be referenced by a settings file must import cleanly from the runtime root with no checkout PYTHONPATH help. If the boot gate fails, no settings file is written.

The installer also wires a second pre-tool guard hook for Claude Code, codex, and gemini. The guard reads a live-path registry only from CF_LIVE_PATH_REGISTRY (or ORCH_LIVE_PATH_REGISTRY) — there is no built-in default path, and with neither set the guard is inactive with a loud warning; see config/live_path_registry.example.json for the expected shape. When the registry is present, destructive git/filesystem commands targeting a registered live checkout are denied and the operator must cut a worktree first. Registered worktree roots remain allowed. If the registry is absent, unreadable, or the hook cannot parse the command, it allows the tool call and emits a loud warning rather than wedging every tool call.

Grok (xAI grok-cli) should use the dedicated global hook file ~/.grok/hooks/cf-notify.json so SessionStart can mark idle at boot. See docs/grok-hooks.md.

Wake packets are injected through lifecycle hooks when the orchestrator endpoint is enabled: SessionStart supplies scoped state at session boot, UserPromptSubmit refreshes it at the start of each submitted prompt, and PostToolUse appends it to drained notification deliveries. All wake-packet fetches are fail-open; if the orchestrator API is unavailable, normal hook behavior continues without context injection.

Run The Daemon

Run one daemon per machine that hosts Claude Code tmux sessions:

bash scripts/start_notify_daemons.sh start
bash scripts/start_notify_daemons.sh status
bash scripts/start_notify_daemons.sh stop

The daemon writes ${NOTIFY_KEY_PREFIX:-taey}:_notify_daemon:heartbeat from an independent timer thread, not from the delivery loop. The delivery loop separately advances ${NOTIFY_KEY_PREFIX:-taey}:_notify_daemon:delivery_progress; watchdogs should treat a fresh heartbeat as process liveness and the progress cursor as delivery-loop movement. A slow broadcast fan-out that keeps advancing progress is healthy; sustained delivery-progress staleness is the delivery-stall signal.

Smoke the Redis round trip:

taey-notify session-b "README stranger round-trip" --from session-a --allow-unregistered-target
taey-ack --node session-b --peek
taey-ack --node session-b

Usage

taey-notify session-b "build is ready"
cc-fleet-notify session-b "same command through the alias"
taey-notify session-b "production deploy failed" --type escalation
taey-notify session-b "cycle done" --type heartbeat --priority low

Normal sends fail loud unless the target passes the three-check reader-readiness gate: reader signal exists through tmux or first-class headless/line-reader identity; queued mail is zero or visibly draining; and the reader is active, explicitly idle with an empty queue, or a headless reader with fresh activity or recent drain evidence. For canonical Taey line readers, turns_open > 0 plus at least one unexpired active_turns lease is authoritative active-reader evidence when the queue is empty or visibly draining. Use --allow-unregistered-target (alias --allow-readerless-target) only when you are intentionally pre-provisioning a Redis inbox for a later direct reader.

Peer defect/status/result reports may omit the target. In that report path, NOTIFY_TARGET remains an explicit override; otherwise the sender's parent is resolved with the same rule as the Stop hook (taey:<node>:parent, then <parent>-codex / <parent>-gemini / <parent>-grok suffix stripping):

TAEY_NODE_ID=weaver-codex taey-notify "DEFECT: debug defect" --type defect
# routes to weaver, not conductor

Read your own inbox:

taey-ack --peek
taey-ack

Protocol

See NOTIFICATION_PROTOCOL.md.

The Redis key layout is:

${NOTIFY_KEY_PREFIX:-taey}:SESSION:inbox
${NOTIFY_KEY_PREFIX:-taey}:SESSION:notifications
${NOTIFY_KEY_PREFIX:-taey}:notify:SESSION:orch
${NOTIFY_KEY_PREFIX:-taey}:SESSION:idle
${NOTIFY_KEY_PREFIX:-taey}:SESSION:last_activity
${NOTIFY_KEY_PREFIX:-taey}:SESSION:last_tool_activity
${NOTIFY_KEY_PREFIX:-taey}:SESSION:tool_running
${NOTIFY_KEY_PREFIX:-taey}:SESSION:tool_running_at
${NOTIFY_KEY_PREFIX:-taey}:SESSION:turns_open
${NOTIFY_KEY_PREFIX:-taey}:SESSION:active_turns

Syntax Check

make syntax

License

Apache-2.0

About

Autonomous wake for multi-Claude-Code-instance fleets. 4 hooks + Redis-backed inbox + daemon. Instances notify each other; idle ones get woken without a human in the loop. Sibling to claude-code-api-watchdog + mcp-reconnect.

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages