Skip to main content

Architecture

┌──────────────────┐ hook events (HTTP POST)
│ claude session A │ ────────────────────────┐
└──────────────────┘ │

┌──────────────────┐ ┌──────────────────────┐
│ claude session B │ ──────────────▶│ ccmon-daemon │
└──────────────────┘ │ (Python, asyncio) │
│ │
┌──────────────────┐ blocks 540s │ - per-session state │
│ ccmon-approve │ ──────────────▶│ - SQLite event log │
│ (per perm req) │ ◀──── decision │ - HTTP API on 7777 │
└──────────────────┘ │ - notify-send │
└──────────┬───────────┘


┌──────────────────────┐
│ CLI client / UI │
└──────────────────────┘

Pieces

  • ccmon-daemon — long-running Python (asyncio) process. Maintains authoritative session state in memory; mirrors to SQLite for crash recovery; serves an HTTP API on 127.0.0.1:<port> and a WebSocket for the UI.
  • ccmon-event — a tiny non-blocking hook script. Claude Code invokes it for SessionStart, PreToolUse, PostToolUse, Notification, Stop, etc. It POSTs the payload to /event and exits in milliseconds.
  • ccmon-approve — the blocking hook for PermissionRequest. It POSTs to /approval and waits up to 540 s for a decision, then returns allow / deny / ask to Claude Code.
  • ccmon — the CLI client.
  • ccmon-ui — the Tauri desktop app. Connects only over the WebSocket.

Identification

Sessions are identified by Claude Code's own session_id (a UUID present in every hook payload). The daemon never spawns claude — it only observes.

State

  • In memory — the authoritative copy of every session, every pending approval, and the recent event window.
  • SQLite at ~/.local/state/ccmon/ccmon.db — every event, every approval decision, every rule definition, persisted for crash recovery and history.
  • Filesystem port file~/.local/state/ccmon/port carries the daemon's bound port for the CLI and UI.

Fail-safe

If the daemon is down:

  • ccmon-event exits 0 silently — Claude Code keeps running.
  • ccmon-approve exits 0 silently — Claude Code shows its own in-terminal permission prompt.

You never lose a session because the daemon crashed; the worst case is that the desktop UI is bypassed for the duration.

Concurrency caps

Two ceilings to be aware of:

  • claude -p summarizers — capped at 2 concurrent to avoid hitting your rate limits. Summaries cache for 30 s.
  • Approval blockccmon-approve blocks at most 540 s before falling back to ask. Claude Code's hook timeout is 600 s; we cut off slightly under so the fallback is clean.

Recursion guard

When the daemon spawns claude -p for a summary, it sets CCMON_SUMMARIZER=1 in the child's environment. Hook scripts no-op when they see it — that's how summaries don't show up as new sessions inside ccmon.