Open source MIT · Linux / macOS · runs on the Python standard library

CC-Monitor: Monitor & audit every action
Claude Code takes on your computer.

CC-Monitor is a small tool I wrote for myself. It records every file Claude Code reads or writes, every shell command it runs, every network connection it opens. The obviously dangerous stuff gets blocked. The borderline stuff asks you first. Everything else is quietly logged so you can go back and look.

$ git clone https://github.com/cn0xroot/CC-Monitor.git && cd CC-Monitor && ./install.sh
87built-in rules, edits apply instantly
2layers: hooks + kernel eBPF
0third-party Python dependencies
8Web UI pages
http://127.0.0.1:9999 — Home
CC-Monitor Web UI home page
PreToolUse · blocked rm -rf ~/projects block
Needs your OK sudo apt install … confirm
Why this exists

A few questions that kept me up at night

Letting an AI agent write code is great. It is also really running shell commands on your box and really writing to your files. After a while I realised I couldn't answer a single one of the questions below. This project is me answering them one at a time.

01

What commands did it just run?

Dozens of tool calls scroll past, and by the time you look up all that's left is "Done". Want to go back and check? There's nowhere to check.

Every tool call's command, path, arguments and decision lands in a local SQLite database. CC-Monitor tail shows it live, with commands syntax-highlighted.
02

What if it fat-fingers an rm -rf?

Claude Code has its own permission prompts, but after you've clicked "always allow" a few times they stop meaning much. The one dangerous command is buried in a pile of normal ones.

Dangerous deletes, disk overwrites, curl | bash, reverse shells, writes into ~/.ssh: denied outright, no matter what you clicked "always allow" on before.
03

Did it read my .env or my keys?

The model reads .env, ~/.ssh/id_rsa, your browser's password store, and you get no notice at all. Maybe it just "took a look". Maybe it already put the contents in a commit.

Sensitive-file reads have their own rules, and not just for the Read tool. Going around it with cat/grep/python -c open() gets caught too. Writing AWS/GitHub/Anthropic-style secrets into a file is flagged as well.
04

What if the hook itself gets changed?

Every hook-based monitor rests on one assumption: Claude Code honestly calls it. Edit settings.json and the monitoring quietly disappears, and nobody notices.

On Linux a second, independent eBPF probe watches the claude process tree's execve/connect straight from the kernel and cross-checks against the hook log. Probe saw it, hook didn't log it: flagged.
05

Where did it connect to?

The model runs curl, git clone, pip install. Where is the data going? Packet capture means installing a CA cert; a proxy is too much ceremony.

eBPF grabs the destination IP:port from connect() and the hostname from getaddrinfo. No TLS termination, no certificate. The Web UI has a detail table, GeoIP lookups and a world map.
06

What was it thinking?

Hooks only see "which tool was called". They can't see the model's thinking or the full conversation. When something goes wrong, you're reconstructing from memory.

AI Tap reads the transcript file Claude Code already writes locally and lays out text / thinking / tool_use / tool_result entry by entry. No packet capture, no man-in-the-middle.
What it does

In one line: see it, stop it, look it up later

Underneath it's one Python script plus a rule table. Everything else is stacked on top. It works without the Web UI; it's a lot nicer with it.

🛡

Block the high-risk stuff

Bash / Write / Edit calls that hit a block rule are refused, and the reason is echoed back to Claude Code. It knows why it was stopped and usually picks a safer route.

🙋

Ask on the borderline stuff

Medium-risk operations prompt in the terminal for a y, plus a desktop notification. With the Web UI running there's a copy on the page too. Answer wherever; first answer wins. The prompt leads with one plain-language line on what the operation is and why it needs you; the rule id is small print. No tty or timeout means deny.

👁

Too noisy? Dial it down to watch-only

Three intervention levels, switchable any time. Enforcing is the default: it blocks what it should and asks about the rest. Permissive still evaluates every rule and still records the risk level and which rule matched, but never blocks and never prompts. Off doesn't even record — the same as not having it installed. The pair people confuse is the last two: neither one stops you, the difference is whether there's anything left to look at afterwards. If you just want fewer interruptions, pick Permissive, not Off. From the CLI: CC-Monitor audit permissive; in the browser it's one click.

📜

Full audit trail

Every event goes to SQLite with the complete tool_input/tool_response. CLI and Web UI read the same data. One click archives it using SQLite's own backup().

🔍

Bypass detection

CC-Monitor verify takes what the probe saw in the kernel and matches it against what the hooks logged in the same time window. The ones that don't match are someone (or some model) working around the monitor. Linux only — this layer needs kernel-level execve observation, and the macOS probe uses nettop, which covers network only. On macOS the command says plainly that the platform can't do this check, rather than handing back an empty "no suspicious records".

🌐

Network destinations

Destination IP, port, hostname, bytes up/down, connection count, and a location if you have a local GeoIP database. The map is drawn in WebGL2 with no map-tile service behind it.

💬

AI Tap

Pick a session and see the full content sent to and from the model: text, thinking, tool calls, tool results, token usage. This is the part the audit log deliberately hides, on its own page.

🖥

A terminal in the browser

node-pty spawns a real PTY, xterm.js renders it, WebGL when available. Multiple sessions can be tiled into a grid view on one screen.

📊

Account & quota

Quota side: 5-hour session window, weekly quota, per-model weekly quota and reset times, each with an hourglass showing how much of that window is left. Read from the OAuth credential Claude Code already stores, queried from Anthropic's own endpoint — the same source ccstatusline uses. Account side: 17 rows, eight identity fields plus four identifiers plus this machine's Claude Code state (version / install method / auto-updates / configured MCP count). One click turns the name, email, org name and the four identifiers into ***, which is all you need before a screenshot.

🍎

Linux and macOS

Hooks and the rule engine are identical on both. The system-layer probe is eBPF on Linux and the built-in nettop on macOS (no root, network only). Verified on real Apple Silicon.

🧭

Cross-workdir detection

Regexes can't see the cwd, so this layer fills the gap: every path a tool call is about to touch is resolved to an absolute path and compared with the project root — Bash commands are split into sub-commands, cd is tracked, redirects and rm / cp / tee-style writes are recognised. Anything outside is tiered by location (hidden home config, other users, system dirs, other projects) × read / write: writes to sensitive places ask first, the rest is logged. Reads inside system dirs, temp dirs and Claude's own state dir don't count.

How it works

No magic, just two layers doing their own jobs

Above was "what". This is "how". Every item maps directly onto the source; the long version is in DESIGN.en.md.

APPLICATION LAYER · CLAUDE CODE HOOKS (primary; every tool call goes through here) Claude Code about to call a tool Bash / Write / Edit / Read… CC-Monitor-hook stdin ← JSON payload one-shot subprocess PreToolUse / PostToolUse policy.evaluate() rules.json, ordered list regex, first-match-wins → block / confirm / log Decision exit 2 = deny /dev/tty waits for y or web / desktop notif. back to Claude Code SYSTEM LAYER · KERNEL PROBE (second line; needs no cooperation from Claude Code) CC-Monitor-probe Linux: bpftrace / eBPF tracepoint execve · connect uprobe getaddrinfo · tcp_sendmsg macOS: nettop, network only Process tree comm == "claude" sched_process_fork flag inherited by children verify what the probe saw vs what hooks logged mismatch → flagged ~/.cc-monitor/events.db (SQLite) hook_pre / hook_post / probe events / approvals, one database CLI tail · rules · stats · verify Web UI / Electron Express + ws + node-pty

hookHow interception works

  • Before and after every tool call, Claude Code writes a JSON payload to the hook command's stdin and waits for it to exit.
  • CC-Monitor-hook exiting with code 2 means deny. Whatever it wrote to stderr is shown back to Claude Code.
  • It's a one-shot subprocess per call, not a daemon. There's no "the monitor died and now nothing is enforced" failure mode, and you never restart anything after editing rules. The next call picks them up.

rulesRule engine

  • An ordered list, tried top to bottom, first match wins. Specific rules go before general ones.
  • Each rule says which tools it applies to, which tool_input field to read, a regex, a risk level and an action, plus a human-readable title / desc the approval prompt uses to explain itself.
  • Copied to ~/.cc-monitor/rules.json on first use; edit it however you like. Rules added by later versions merge in by id, and anything you edited or deleted is left alone.
  • When the rule set changes, historical events get re-tagged in the background under the new rules (only risk and matched rule; the original allow/block decision is never rewritten), so the home-page counts don't carry stale hits forever.

eBPFSystem-layer probe

  • probe_linux.bt attaches to the kernel's execve/connect tracepoints, recognises Claude Code by comm=="claude", then listens for sched_process_fork and passes a "being monitored" flag down to every descendant. Renamed child processes are still tracked.
  • Hostnames don't come from reverse DNS: a uprobe:libc:getaddrinfo records the name the moment the application resolves it. Plenty of cloud egress IPs never had a PTR record, so reverse lookup would find nothing.
  • Byte counts come from tcp_sendmsg/tcp_cleanup_rbuf, aggregated per (ip, port).
  • verify normalises quotes when matching, to cope with how zsh's snapshot wrapper escapes them.

tapWhere AI Tap gets the conversation

  • The hook payload includes a transcript_path pointing at the conversation JSONL Claude Code writes locally.
  • Read that file, parse the user/assistant/tool_use/tool_result entries, and the whole conversation comes back. No capture, no certificate, no proxy.
  • Context compaction count is a real detection of compact_boundary events in the transcript. Context usage % is estimated against a 200K window, because Claude Code doesn't tell the hook the exact size.

quotaWhere the account quota comes from

  • The OAuth token Claude Code itself stores (Linux: ~/.claude/.credentials.json; macOS: the login Keychain), used to call api.anthropic.com/api/oauth/usage.
  • Same credential and same endpoint ccstatusline uses. I did not build a separate usage tracker.
  • The account profile is read from the local ~/.claude.json: identity fields and the two UUIDs come from its oauthAccount, while machine ID / user ID / install method / auto-updates / the MCP list come from the same file's top level. subscriptionType and the rate-limit tier also exist in the OAuth credential and can disagree with their oauthAccount counterparts — the disagreement is itself a signal, so both are shown. All of this is local file reads, zero network calls.
  • The one exception is the Claude Code version: claude --version is run once for an authoritative answer and cached in-process. If that fails it falls back to the local versions directory or the config file, which are approximations — labelled as such in the UI rather than passed off as exact.

ptyWeb terminal

  • node-pty spawns a real pseudo-terminal, no different from opening a terminal window locally, then types claude\r into it.
  • The first time Claude Code opens an untrusted directory it shows a "trust this folder?" prompt defaulting to "No, exit". That banner is detected and answered Yes automatically. Otherwise the next stray Enter would silently exit Claude Code while the terminal still looked perfectly fine.
  • "New Window" is the same thing minus the claude\r, for when you just want a shell to run a script.
Rules

87 default rules, three risk tiers, three actions

Rules are regexes, nothing more. I tried to tier them by "how likely is this to wreck the machine or walk off with data", not by "does this look suspicious", because if it nags you'll turn it off.

33high: 15 blocked outright, 18 ask first
30medium: 22 ask first, 8 log only
11low / info: mostly log only
ActionBehaviourUsed for
blockDenied outright; reason echoed to Claude CodeThings an AI has no legitimate reason to do: rm -rf /, disk overwrites, fork bombs, reverse shells, writing authorized_keys
confirmTerminal + desktop notification + web page wait for a y; no tty or timeout means denyMaybe legitimate but you should know: sudo, global npm installs, git push --force, crontab / systemd changes, editing Claude Code's own config
logAllowed, but recordedNormal but worth a trace: local pip/npm installs, sensitive-file reads, ordinary kill, outbound requests
notifyNo block, no prompt; just mirrors Claude's question to the approvals pageAskUserQuestion-style clarifying questions, with your answer recorded alongside
  • blockDangerous deletes, dd over a disk, curl | bash, encoded payloads piped to a shell, recursive chmod 777, webshell patterns in written content
  • blockWrites to ~/.ssh/, credential files, system directories; disabling security controls (firewall, SELinux and the like)
  • confirmkill/pkill aimed at CC-Monitor's own probe or hook (a generic kill is log-only, otherwise it's too noisy)
  • confirmSSH tunnels / reverse proxies, C2 frameworks, post-exploitation tools, mining-pool domains, Docker privileged mode or mounting docker.sock
  • confirmDROP / DELETE / TRUNCATE / FLUSHALL sent straight to a database CLI; reading browser credential stores; reading /proc/*/environ
  • confirmAWS / GitHub / Anthropic / OpenAI / Slack / Stripe key prefixes or private-key headers showing up in written content
  • confirmWriting outside the current project into a sensitive location: hidden home-dir config (~/.xxx), another user's home, system directories. Reads/writes into other project directories and reads of system directories are log-only — the boundary is the project root (.git / CLAUDE.md markers), not the hook's cwd
  • logReading shell history, .env or cloud VPN configs via cat / grep / history; dumping the whole environment with env / printenv
// one entry from ~/.cc-monitor/rules.json
{
  "id": "curl_pipe_shell",
  "risk": "high",
  "action": "block",
  "title_en": "Download piped straight into a shell",
  "desc_en": "Content fetched from the network is executed without ever being saved…",
  "tools": ["Bash"],
  "field": "command",
  "pattern": "(curl|wget)\\b.*\\|\\s*(sudo\\s+)?(ba|z|)sh\\b",
  "match": "search"
}

// two match modes:
//   search  — regex over the whole command text. For "mentions X anywhere"
//             rules (paths, redirections, download piped into a shell).
//   segment — split the command on ; && | first, strip sudo/env/xargs/time
//             and path prefixes, anchor the regex at the start of each
//             sub-command. For "is this program actually being run" rules
//             (package installs, sudo). Recurses into bash -c "..." and
//             osascript shell bodies.

//   workdir — not a regex: looks at where the paths this call touches sit relative
//             to the project root (cross-workdir detection); scopes / access pick
//             the tier, ignore_paths whitelists.
// title / desc are for humans: the approval card and terminal prompt lead with them;
// the rule id is small print.
// Save and you're done; the next tool call uses the new rules.
// New defaults from later versions merge in by id; your edits are kept.
Rules are regexes, so there is always a way to phrase around them and always some normal command that gets caught by mistake. It's a net, not a wall. For code and repos you genuinely don't trust, keep the containers, read-only mounts and dedicated accounts you'd have anyway.
UI tour

What each Web UI page is for

The Web UI is a standalone Node service under webui/, listening on 127.0.0.1:9999 by default. Eight pages, each answering one kind of question, all sharing one header pattern — a short title plus an ⓘ that holds the long explanation on hover. In top-bar order:

Home

The question it answers: "How are things right now, how much did it do today, and what's worth a look?" A compact dashboard: a status strip on top, then groups for Account & quota / Security / Activity / Commands / Distribution. Nearly every card opens a detail view; the ⓘ next to each group title holds the full explanation on hover.

  • Status stripFive horizontal cards: intervention level (enforcing / permissive / off, plus how many claude processes were found and which OS user each runs as — a user mismatch with the Web UI is flagged loudly, since each side resolves ~/.cc-monitor/ from its own $HOME), sessions monitored (with active Web UI terminals and the vital-sign badge), pending approvals (click jumps to the desk), blocked + suspected bypasses, session quota remaining. The "how are things" numbers that used to be scattered across the page, in one row.
  • ToolbarThe intervention-level segmented control, archive / clear current data, sync update and allow-remote-access on one line, each group with an ⓘ carrying the explanation that used to be a paragraph. All three levels sit side by side, click one to go straight there, and the active one is highlighted in its color (Enforcing purple / Permissive green / Off yellow), with a line underneath that follows whichever level is active. Permissive still evaluates rules and logs, but never blocks or asks; Off does nothing at all and asks for confirmation before switching.
  • Account & quota17 rows of account info on the left: identity (name / email / org / role / plan / rate-limit tier / billing / two dates), four identifiers (account UUID / org UUID / user ID / machine ID, truncated with the full value on hover), and this machine's Claude Code state (version / install method / auto-updates / configured MCP count). "Hide sensitive info" beside the heading turns the name, email, org name and the four identifiers into *** in one click (tooltips included), which is what you want before a screenshot; the rest are account attributes rather than identity and stay visible. On the right, on the right the session-quota-remaining, weekly and per-model weekly cards plus the limits table — one two-column row. Per-model quotas the API doesn't return (plans without separate Sonnet / Opus limits) are simply not drawn, rather than shown as "-".
  • SecuritySix red cards side by side: sensitive operations, sensitive data, advanced threats, cross-workdir operations, screenshots / viewed images, reverse-engineering tools, each with a one-line coverage hint underneath (the cross-workdir card shows live read / write / sensitive-location counts). Approximated from matched rules, not exact; click for the category breakdown.
  • ActivityTotal events, tool calls (hook_pre only), subagent spawns (grouped by subagent_type), AI trajectory (targets the probe observed plus ones inferred from wget/curl/git clone/ssh command text, tagged "inferred"), Skill calls; the three low-frequency counters MCP / Glob·Grep / TodoWrite share one card with each number clickable on its own. Two boxes below: file operations (read / write / edit / delete) and software installs across seven groups (pip / uv / other Python package managers / JS package managers / system package managers / toolchain and version managers / other — only the ones writing into the system ask first; those landing in a project or home directory are just logged).
  • Command operationsSeven dense cards: GitHub, SSH, downloads, Docker, archives, network diagnostics, process management / backgrounding; click for per-category totals and every command.
  • DistributionThree bar lists: event type / risk level / decision. The six event types each get their own color, from a palette run through readability checks (lightness band, chroma floor, adjacent-pair CVD separation, contrast against the surface), with separate values for light and dark themes.
  • Screenshot auditClaude Code has no screenshot tool, so this is identified from three signals: a Bash call to a screenshot CLI, Read opening an image file, or an MCP / computer-use screenshot action. Shows only time and command; the image itself is never read or rendered.
Session list drilldown
Session list, from clicking "total sessions"
Event type breakdown
Event type breakdown, from clicking "total events"
Blocked high-risk operations

Status

The question it answers: "Which model is each session on, how many tokens has it burned, how much quota is left?"

  • Account & quotaThe same two-column layout as Home, with the same 17 rows of account info; on the right the 5-hour session window, weekly quota and per-model weekly quota cards plus the limits table and reset times. Each reset time sits beside an hourglass whose remaining sand is the time left in that window, colored to match the card's health, so you can feel how full it is without reading the number. From api.anthropic.com/api/oauth/usage, same as ccstatusline.
  • Model usage tableToken usage summed per model (Sonnet / Opus / Fable…) across every monitored session.
  • One row per sessionModel, tokens, throughput (tok/s, estimated from the transcript), and a Σ Total / Cached summary using ccstatusline's accounting (total = input + output + cached; cached = cache-read + cache-creation).
  • Context usage %Estimated against a 200K window. Claude Code doesn't tell the hook the exact size, so it's an approximation.
  • Context compactionsThis one is real: it detects compact_boundary events in the transcript.
  • Alsocwd, git branch, uptime, blocked-operation count.
Known risk with the quota feature: it reads the local credential Claude Code itself stores. It doesn't modify the file, but in some edge cases it may invalidate the existing login and force a re-login. If you're mid-way through a long task, decide whether you can live with logging in again before turning it on.

Network

needs CC-Monitor-probe running

The question it answers: "Where did the Claude Code process tree actually connect, and how much moved?" All from the system-layer probe, not packet capture.

  • Destination listIP, port, hostname, bytes up / down, connection count, location. Hostnames come from the getaddrinfo uprobe on Linux and fall back to reverse DNS on macOS.
  • Connection detailThe count on each row is clickable, as are the two summary cards (total connections / distinct IPs): time, originating process and PID of every connection.
  • World mapWebGL2, equirectangular projection, a bundled low-res coastline, no tile service. Each connection draws an arc from a schematic anchor to the destination with a light dot travelling along it, in whichever direction moved more bytes. Falls back to Canvas 2D when WebGL2 is missing.
  • Why the anchor is at (0,0)That's open ocean. I deliberately don't ask a third party "what's my public IP" to locate you. It's a drawing origin, not your position.
  • GeoIPLocal database only, no per-IP API calls. install.sh downloads the no-signup DB-IP Lite by default; MaxMind GeoLite2 works too. Without one, the location column and map are just empty and the page says so instead of inventing data.
  • Probe not runningThe page is empty, not broken.

Terminal Sessions

The question it answers: "Can I open Claude Code right here without switching windows?" Yes.

  • New SessionPick a working directory; the backend spawns a PTY via node-pty and types claude\r. The first-time "trust this folder?" prompt is answered Yes for you.
  • New WindowSame directory picker, minus the claude\r. For when you just want a shell.
  • Renderingxterm.js + WebGL addon, GPU when available, Canvas otherwise. The top bar shows which one is in use.
  • Grid viewThe sidebar switches to a herdr-style grid with every live session on one screen. Click a pane to send keyboard input to it.
  • Vital-sign dotEach session has a dot that fades continuously with activity, flatlines after an hour idle, and keeps a colour floor so idle sessions never go fully grey.
This page means the Web UI can spawn terminal processes on your machine, and it has no authentication. That's why it binds to 127.0.0.1 only. The "allow access from other devices" switch on Home only sets a flag; the real boundary is the address the process bound to at startup, which a web page can't change. To really expose it, an admin sets CC_MONITOR_WEBUI_HOST=0.0.0.0 and restarts, and only then does the switch do anything.

AI Approvals

The question it answers: "Is anything waiting on me?" Mirrors Claude Code's "proceed?" prompts into the browser, in the spirit of Vibe Island's Allow/Deny card in the Mac notch, except cross-platform.

  • Two kinds of requests land hereOperations our own rule table marks confirm; and Claude Code's native "Do you want to proceed?" (the PermissionRequest hook event), for calls that matched no rule but Claude Code's own permission system wants a human on.
  • Plain language firstEach card leads with a one-line title such as "Confirm: git force push", then a line on why it needs you ("overwrites remote branch history; commits others already pulled may be lost"); the rule id and tool name are demoted to a small last line. All 87 rules carry these two sentences (title / desc — edit rules.json and the page follows); Claude Code's native prompts get a per-tool sentence. The terminal prompt and desktop notification carry the same explanation.
  • First answer winsThe same request can be answered with y/N at the terminal or with a click here. Allowing from the web page makes Claude Code skip its own popup, so you're never asked twice.
  • The safety net staysIf nobody answers within 90s, or you press Enter at the terminal, the request is handed back untouched to the native dialog. Installing CC-Monitor never removes that.
  • OptionsAllow once, deny once, allow and don't ask for 10 / 30 minutes, always allow for this session.
  • Desktop notificationsBrowser Notification API, works with the tab closed, click to jump back. The Electron build goes a different route: system alert sound + Dock bounce + badge count.
  • History tableEvery resolved request is kept: time, session, tool, matched rule (shown as its plain title, hover for the id), outcome, resolved via. For AskUserQuestion-type records it also captures what you actually answered at the terminal. "Clear current data" leaves this table alone.

Log Audit

CLI equivalent: CC-Monitor tail

The question it answers: "What operation happened, what risk level, allowed or blocked?" This is the security-audit view, and the content is deliberately redacted.

  • Full-width live listFilterable by session; the dropdown shows "folder · model · short ID" instead of a raw UUID.
  • Same translation as the CLIRaw JSON becomes "event type + summary + result", Bash commands syntax-highlighted.
  • What you seeWrite / Edit show only "path (N bytes)", TodoWrite shows only an item count, screenshots show only metadata. Never the actual content read or written.
  • ColoursRisk / operation / status badges use fixed saturated colours that don't follow the theme; high-risk rows are bold red.
  • How it differs from AI TapLog Audit reads events.db and answers "what did it do". AI Tap reads the transcript and answers "what did it say and think". Two data sources with two very different levels of restraint, not two views of the same thing.
Log Audit page

AI Tap

CLI equivalent: CC-Monitor tap [--session ID] [-f]

The question it answers: "What did Claude actually say and think in this session?" For recaps and debugging. Content is not redacted.

  • Pick a session firstChoose one session from the dropdown at the top right, or "★ All sessions" to merge the most recent slice of every session in time order. With nothing selected the page says so instead of staying blank.
  • Entry by entrytext / thinking / tool_use / tool_result / token usage, each field colour-coded. Including the thinking Log Audit can never show, because hooks only see the world at the tool-call boundary and thinking never crosses it.
  • Data sourceThe transcript JSONL Claude Code writes locally, via the transcript_path in the hook payload. No capture, no MITM.
  • About empty "thinking"Newer models (Sonnet 5 / Opus 5 / Opus 4.8 / 4.7) default to display: "omitted" at the API, so the thinking body never reaches the response and never reaches the local transcript. Opus 4.6 / Sonnet 4.6 and older do have it. When there's a body it expands fully; when there isn't, the page says why instead of pretending.

Historical Data

The question it answers: "Can I still see the data from before I cleared it last week?" Yes, if you archived before clearing.

  • ListEach archive's time, event count, session count and time range. Open it or delete it.
  • How the snapshot is takenSQLite's own backup() API, not a file copy. It correctly handles data still sitting in the WAL journal, which a plain cp would miss.
  • Opening oneBrowse its events the same way Log Audit does, paged.

Top bar & appearance

Besides the page tabs, a few things live in the top bar permanently.

  • Intervention level pillEnforcing / Permissive / Off, color-matched to the segmented control on Home, visible from every page.
  • Approvals badgeThe number on the "AI Approvals" tab: how many requests are waiting on you.
  • RendererWhether the terminal is currently on WebGL or Canvas.
  • Theme dropdown10 palettes: Brand / Dark / Light / Dracula / Nord / Midnight / Ocean / Forest / Sunset / Rose.
  • ⚙ AppearanceThemes as a swatch grid; interface font (system / monospace / serif / rounded / Kaiti / Heiti / Songti); font size 12 to 18px on a slider, with the whole app scaling proportionally via rem. Live preview, saved to localStorage.
  • CJK fonts aren't bundledA full CJK font file is 17 to 21MB; making the first font switch that slow isn't worth it. The options are plain font-name references that work where the system already has the font.
  • EN / 中 toggleTranslates the UI chrome (nav, buttons, titles, empty states, risk / status labels), not the data (command text, tool output, transcript content).
  • Static assets are no-storeThis UI is still iterating fast; refresh after a code change and you get the latest, no cache to clear.
Command line

Works without a browser

Three executable scripts under bin/, standard-library Python, no build step. make install symlinks them into /usr/local/bin; without it, ./bin/CC-Monitor runs in place.

CommandWhat it does
CC-Monitor tailLive event view, Ctrl+C to quit. -v adds the raw JSON. Colour is automatic on a real tty; NO_COLOR or CC_MONITOR_COLOR=never turns it off.
CC-Monitor auditSwitch the intervention level. start (enforcing) / permissive (logs and evaluates, never blocks or prompts) / stop (off, not even logging) / status. permissive also answers to observe, and the old name pause still works.
CC-Monitor rulesList the rules currently in effect.
CC-Monitor statsCounts by risk level / decision.
CC-Monitor verifyCommands the probe saw but the hooks never logged. Linux only.
CC-Monitor tapTerminal AI Tap. --session ID picks a session, -f follows.
CC-Monitor rematchPreview re-tagging historical events under the current rules; --apply does it for real.
CC-Monitor workdirList operations where the AI read or wrote files outside the current project — per-rule totals, then one row each. --limit N.
CC-Monitor-hookThe hook Claude Code calls. You never run it by hand.
sudo CC-Monitor-probeSystem-layer probe. Root for bpftrace on Linux; no root on macOS, where it switches to nettop.
Environment variables: CC_MONITOR_HOME moves the data directory (default ~/.cc-monitor/); CC_MONITOR_COLOR forces colour on or off; CC_MONITOR_WEBUI_HOST changes the Web UI bind address; CC_MONITOR_GEOIP_DB points at a GeoIP database.
CC-Monitor tail
14:02:11 PreToolUse  Bash  git status
         allow  —
14:02:13 PostToolUse Bash  exit 0
14:02:40 PreToolUse  Write ./src/api.ts (1.2 KB)
         allow  —
14:03:02 PreToolUse  Bash  npm install -g some-cli
         confirm npm_global_install [medium]
         waiting for terminal (y/N)… y → allow (via web)
14:05:18 PreToolUse  Bash  curl -fsSL https://x.y/i.sh | bash
         BLOCK   curl_pipe_shell [high]
14:05:19 PreToolUse  Read  ~/.ssh/id_ed25519
         BLOCK   sensitive_file_read [high]
14:06:44 PreToolUse  Bash  rm -rf ~/projects/old
         BLOCK   dangerous_delete [high]
14:07:01 PreToolUse  Bash  cat .env
         allow  sensitive_file_read_bash [medium, log]
Install

Two minutes to install, one to remove

Every step the installer takes is idempotent and never overwrites config you already have. Hooks are merged into the arrays in ~/.claude/settings.json, de-duplicated by command string; your existing hooks are untouched.

Just the core

Interception + audit + CLI. No npm or Python dependencies; it only registers the hooks in settings.json.

git clone https://github.com/cn0xroot/CC-Monitor.git
cd CC-Monitor
python3 install.py
# then restart Claude Code; only new sessions read the new config

The whole thing

One script, 5 steps: ccstatusline, hook registration, Web UI deps, probe check, GeoIP database. Each can be skipped individually.

./install.sh
./install.sh --skip-ccstatusline --skip-geoip
./start.sh   # Web UI at http://127.0.0.1:9999

Want the system-layer probe

Linux needs bpftrace; macOS needs nothing. The probe is started by hand for now, there's no systemd unit yet.

sudo apt install bpftrace   # Debian/Ubuntu
sudo ./bin/CC-Monitor-probe
# macOS, no sudo:
./bin/CC-Monitor-probe

Requirements

Hooks: Python 3.8+, standard library.
Web UI: Node.js ≥ 22. Not a number I picked; it's what better-sqlite3 declares in its engines field, and older Node fails halfway through install.
Desktop: npm run electron, Electron 44+. 33.x segfaults on startup on Zen 5, so don't roll it back.
What I've actually verified on: Ubuntu 24.04, Node 22.17, Python 3.13, Apple Silicon M4.

System path install / uninstall

sudo make install            # /usr/local/lib/cc-monitor + /usr/local/bin
sudo make install PREFIX=/opt/cc-monitor
sudo make uninstall          # code and symlinks only; remove hooks from settings.json yourself

How far each platform goes

CapabilityLinuxmacOSNotes
Hook interception / confirm / audit✓✓Identical
Web UI / CLI / AI Tap / Approvals✓✓Identical
System-layer network observation (IP / port / bytes)✓ eBPF✓ nettopmacOS samples every 2s, no root
Hostname capture✓ getaddrinfo uprobereverse DNS fallbackCloud egress IPs often have no PTR
System-layer execve observation / bypass detection✓✗macOS needs a signed Endpoint Security extension; not done
Desktop packaging✓ AppImage (x64 verified)dmg builds, not fully verifiedDeveloper ID signing needed for real notifications
🧪 Experimental · dev branch

Beyond Claude Code: the dev branch extends the detection surface to every local AI agent

Everything above describes master — Claude Code only, verified by long daily use. The dev branch is wiring the same rules, approval desk, audit log and eBPF probe to other AI coding agents, and widening what the kernel layer can see from "what ran, where it connected" to "which files changed, which ports opened, which session it belongs to". Every agent except Claude Code is in an experimental / testing stage: implemented from each vendor's official docs or source, covered by unit tests, but only Antigravity CLI has had a full real-machine round (it surfaced 9 bugs, all fixed, see the table below), OpenClacky was verified by its contributor through the gem's own hook loader, and the rest have not been verified on real installs. Not recommended for production; to try it, check it out separately with its own data directory and port so the master install is untouched.

🗂

Agent registry: "who is an agent" is data, not code

One cc_monitor/agents/<id>.json per agent: process signature (kernel comm, executable name, argv regex), hook protocol and config path, tool-name and argument-field mappings, session-file location, its own state dirs under home, project-root markers, config-tamper paths. No agent name is hard-coded anywhere; adding one is, in principle, one JSON file, and users can override per field with a same-named file in ~/.cc-monitor/agents/. Borrowed from AgentSight's -c comm list and record -- auto-discovery.

🔌

Hook adapters: nine protocols, one decision path

hook.py became "adapter parses stdin → agent-agnostic decide / approve / log → adapter emits what that agent understands". Other agents' tool names are translated into Claude Code vocabulary before reaching the engine (run_shell_command→Bash, write_to_file→Write, filePath→file_path, …), so the 87 rules, cross-workdir detection, approval desk and Web UI stats are one code path; the original name is kept in the record's native_tool. Deny formats differ: Codex permissionDecision=deny, Gemini {"decision":"deny"}, Cursor {"permission":"deny"}, Antigravity {"decision":"deny"}, the OpenCode plugin, Grok and OpenClacky (rewrite protocol) read exit code 2.

⚙

One command to enable, idempotent, never touches your existing config

python3 install.py --agent codex | gemini-cli | cursor | opencode | zcode | antigravity-cli | grok-cli | openclacky, or --agent all for every agent detected on the machine. Each config file is written from the registry (~/.codex/hooks.json, the hooks block of ~/.gemini/settings.json, ~/.cursor/hooks.json, ~/.zcode/cli/config.json, the cc-monitor group in ~/.gemini/config/hooks.json, ~/.grok/user-settings.json, the OpenCode plugin file, OpenClacky's ~/.clacky/hooks.yml — YAML, the generated block is fenced by a pair of markers, re-runs replace that block and never touch what you wrote yourself); existing entries are never duplicated and other people's hooks are left alone. With no flag the behaviour is byte-identical to master — verified as a no-op against a real ~/.claude/settings.json. --list and CC-Monitor agents show each agent's status (verified / experimental), install and hook state.

🧬

The probe recognises every agent's process tree

The bpftrace script is now a template: every compiled agent's comm (claude / codex / opencode / zcode / agy / grok, …) is rendered into a sched_process_exec probe and recognised in-kernel; node- / python-hosted agents (Gemini CLI, Aider) show the interpreter's comm, so a /proc scan matches them by argv. At start-up, agent trees already running are "seeded" into the watch set — which also fixes master's old "a Claude Code started before the probe is invisible" problem. New roots found while running restart bpftrace to include them (about a 1 s window). Launching agy from a Claude Code terminal makes agy its own root: its hook events are recorded under its own --agent, so kernel events go there too and cross-checking lines up — learned on a real machine, where the old rule flagged every one of its commands as a bypass.

📁

Kernel-level file view: writes, deletes, renames, mkdirs, listening ports

After AgentSight's process_ext probe set: openat with write flags, unlinkat / unlink / rmdir, the renameat2 family, the mkdirat family, plus bind + listen. Delete / rename / mkdir are paired enter→exit and only successful calls are reported — the EEXIST of mkdir -p a/b/c on each ancestor and the ENOENT of rm -f missing are not noise. File-path rules apply to kernel-observed writes too: a pip / npm / script spawned by the agent writing ~/.ssh is invisible to hooks but hits sensitive_file_write here. The agent process writing a file directly with no hook Write / Edit record for the same path within 15 s is flagged hook_bypass_suspected. Ports bound on 0.0.0.0 / :: are marked medium risk. Pure reads are not reported.

🧭

Relative-path resolution: one step further than AgentSight

rm -r, mkdir -p and Python's shutil.rmtree operate level by level through directory fds; the kernel sees unlinkat(5, "f"). AgentSight aggregates the raw path as-is. Here, when openat returns successfully the task's fd table is consulted and, if the inode is a directory (checking O_DIRECTORY is not enough — Python opens directories without it), an OPENDIR line is emitted; together with dup / fcntl(F_DUPFD), fchdir, chdir and fork events, userland keeps "(pid, fd) → directory" and "pid → cwd" tables and resolves every relative path by lookup, never touching /proc (the process is gone within a millisecond, and a reused fd number can point at the wrong directory). Verified on this machine for all three tools, every level correct.

🔗

Kernel events attributed to sessions

On master, probe events carry no session id. On dev the hook process walks up the /proc parent chain from its own parent to the first recognisable agent process (Claude Code: hook ← sh ← claude) and records "session ↔ root pid + start time" in a sessions table; the probe looks each event up by root pid. Three evidence levels — hook parent chain (exact) > explicit CC-Monitor run registration > a guess at probe start by agent + cwd + recent activity — and strong evidence overrides weak, never the reverse. The Web UI now decides session liveness by "is the root process still alive" instead of guessing by cwd.

🎯

CC-Monitor run -- <cmd>: explicit binding

For agents with no hooks and no recognisable process signature (custom scripts, python my_agent.py, things inside containers): registers "this pid is that agent's root" in ~/.cc-monitor/run/<pid>.json, sets CC_MONITOR_AGENT / CC_MONITOR_SESSION for the process tree, then execs the command (same pid, no extra parent). The probe picks it up within 3 s and the registration is cleaned on exit; hooks fall back to the env var when --agent is absent, so a custom agent that speaks the Claude Code hook protocol is attributed correctly.

💬

AI Tap reads three transcript formats

Session-file parsing detects the format by line shape: Claude Code's ~/.claude/projects JSONL; Antigravity CLI's brain/<id>/.system_generated/logs/transcript.jsonl (the real agy 1.2.6 step_index / source / type format — user input unwrapped from <USER_REQUEST>, the extra JSON-quoting layer on tool args removed); Codex's rollout-*.jsonl (from public docs). The Python and JS parsers are kept in sync. Assistant turns are labelled with the agent's name instead of always "Claude". Gemini's whole-file JSON, Cursor's agent-transcripts and the OpenCode / Grok SQLite session stores are not covered yet.

🧹

Noise and performance

Each hook invocation spawns sh → CC-Monitor-hook → python, and Claude Code's status line runs ps / stty / jj root / git rev-parse every few seconds — 68 % of os_exec rows in a real database were these; dev drops them entirely. File events: /proc /sys /dev dropped in-kernel; .git/, node_modules/, __pycache__/, caches, build dirs and each agent's own state dirs excluded in userland; one row per (root, path, op) per 60-second window plus an aggregate at expiry; a breaker at 200 events per second per root. The events table finally has indexes — master has none, so every poll is a full scan past a few tens of thousands of rows.

🖥

Web UI changes appear only when several agents coexist

An agent filter in the top bar — once set, every endpoint sees only that agent: the data layer carries the request's agent through AsyncLocalStorage into each query and rewrites FROM events into a subquery filtered to it, so none of the dozens of SQL statements change; the "Anthropic account & quota" block becomes that agent's "account & environment" panel (install path, version, signed-in account, auth type, whether the hook is installed, session count, models seen; none of them has a local quota API, and the quota card says so). A "Monitored AI agents" home card, agent badges on audit-log rows / the session dropdown / approval cards / the process drilldown (ten colours, experimental ones marked β), a "Kernel-level file / port observation" home card with drilldown, and a choice of which agent to launch in a new terminal session. With only Claude Code in the database all of it is hidden and the UI is identical to master.

🛡

New rules

agent_config_tamper: editing any agent's hook / config files (.codex/hooks.json, .gemini/settings.json, .cursor/hooks.json, .zcode/cli/config.json, .agents/hooks.json, opencode.json and the plugin dir, …) asks for confirmation — tampering with any agent's hook config switches off that agent's application-layer monitoring, which matters more with several agents. The regex is assembled from the registry at load time through an @registry: placeholder, so new agents need no rule edits. The history_read rules cover other agents' session dirs; kill_monitoring_process covers the OpenCode plugin file. Rule count 86 → 87.

How far each agent goes

  • Claude Code ✅ verified — hooks + probe, long daily use, byte-identical behaviour to master.
  • Antigravity CLI (agy, Go) 🧪 experimental — the cc-monitor group in ~/.gemini/config/hooks.json, PascalCase args of run_command / write_to_file mapped. One end-to-end round on 1.2.6 / 1.2.7 here: sudo pip install blocked, agy echoing "tool call denied by pre-tool hook"; crontab -l went through the approval desk, was allowed from the web page and ran; the probe attributed its commands to antigravity-cli and cross-checked them against hook records; AI Tap parses its transcript. That round surfaced 9 issues, all fixed: every command flagged as a "bypass" when launched from inside Claude Code (a nested agent is now its own root); the self-update probe file flagged as a cross-workdir write; PreInvocation firing on every step and flooding empty events; the session cwd recorded as the scratch dir; a flat grey heartbeat (process ↔ session now matched by root pid); Anthropic account and quota still shown when selected (replaced by that agent's "account & environment" panel); the wrong account shown (both agents share ~/.gemini but keep separate credentials — now read from agy's own oauth token); sessions declared ended because agy forks a short-lived same-named child per tool call (same-agent children are no longer roots); an empty model column (the transcript carries none, the hook's modelName is the fallback). The full record is in MULTI-AGENT.md §10.
  • Codex CLI 🧪 experimental — ~/.codex/hooks.json, protocol mirrors Claude Code's, apply_patch split per file; needs [features] hooks = true in config.toml. Not installed, not tested.
  • Gemini CLI 🧪 experimental — hooks block in ~/.gemini/settings.json, run_shell_command etc. mapped; installed here but the account is no longer eligible for this client (it points to Antigravity), so it could not run.
  • Cursor (IDE) 🧪 experimental — ~/.cursor/hooks.json; beforeShellExecution / beforeMCPExecution / beforeReadFile can block, afterFileEdit is log-only; the probe does not apply to an Electron IDE; whether Cursor's CLI runs hooks locally is untested.
  • OpenCode 🧪 experimental — JS plugin bridge ~/.config/opencode/plugins/cc-monitor.js, calls the hook synchronously in tool.execute.before, exit 2 blocks. Not installed, not tested.
  • ZCode (Z.ai / GLM) 🧪 experimental — hooks.events in ~/.zcode/cli/config.json (type: process), protocol mirrors Claude Code's; the current version ignores project-level hooks. Not installed, not tested.
  • Grok CLI (superagent-ai) 🧪 experimental — hooks block in ~/.grok/user-settings.json, implemented from its src/hooks/ source, exit 2 blocks. Never run.
  • OpenClacky (Ruby gem) 🧪 experimental — contributed by @leezii (PR #3): ~/.clacky/hooks.yml, before_tool_use uses the rewrite protocol and exit 2 blocks, after_tool_use can only use the simple protocol and is log-only; the terminal tool's input into an already-open shell — a real bypass path — is mapped to a command and goes through the rules; the process comm is ruby, so the probe matches it by argv. Verified through the gem's own ShellHookLoader: rm -rf / and writes to ~/.ssh/authorized_keys blocked; gem 1.5.15 is installed here and openclacky hook_verify reports both hooks OK, but running the agent needs its API key, so it has not been exercised in a real session. A double-escaped registry regex was fixed on merge (without it, editing hooks.yml would not trigger the tamper rule).
  • Aider / custom scripts 🧪 experimental — no hooks, probe only (by argv or explicit run --); observed, not blockable.

How to try it, how to verify it, what is missing

  • Check it out separately, leave master's install alone: git worktree add ../CC-Monitor-dev dev, then CC_MONITOR_HOME=$HOME/.cc-monitor-dev CC_MONITOR_WEBUI_PORT=9998 ./start.sh. Install other agents' hooks from the worktree (their config files are separate from Claude Code's) and launch those agents pointing at the dev data directory. Do not install a second hook for Claude Code itself — both would fire.
  • Verify an integration: have that agent run a command that must hit a high-risk rule (e.g. rm -rf ~/nonexistent/..); CC-Monitor tail should show an event tagged with that agent and decision=blocked; the "hook" column of CC-Monitor agents turns to "installed".
  • Still to verify on a machine with the agent installed: Codex's hooks flag default and its PermissionRequest format; whether Gemini's {"decision":"allow"} skips its native prompt; whether Cursor's CLI runs hooks locally; OpenClacky in a real agent session (needs its API key); OpenCode's session directory and plugin timeout; the process name of ZCode's bundled desktop runtime; Grok CLI's actual stdin fields.
  • Not done: session-file parsing for Gemini / Cursor / OpenCode / Grok / OpenClacky; the network-traffic page does not follow the agent filter (the traffic table aggregates by IP + port and has no agent column); OpenClacky's after_tool_use uses the simple protocol, which carries no session id, so post events land with an empty session; a TLS metadata layer, OpenTelemetry export, docker:// binding; swapping the probe backend to BCC / libbpf to remove the restart window.
  • Docs: the dev branch README, MULTI-AGENT.md (usage / architecture / per-agent details / adding an agent / troubleshooting) and DESIGN-multi-agent.md (design, including the AgentSight analysis and what was borrowed) — both in Chinese.
Once more: on the dev branch, everything except Claude Code is experimental. Vendor hook protocols change between versions and the adapters follow the docs / source as of writing; after enabling one, verify it yourself as described above and open an issue for anything that does not line up.
What it can't do

Let's be clear about what it isn't

This is in the README too. I don't want anyone installing it and thinking they now have a sandbox.

Product boundaries

  • Only sees what Claude Code initiates. Commands you type yourself, commands run with the ! prefix in the chat, and commands Claude asks you to run because sudo needs a password: none of those are in the audit log. That's the boundary, not a miss.
  • Interception is regex, not semantics. There's always a phrasing that slips through and always a normal command that gets caught. Danger described in natural language isn't recognised.
  • The probe audits; it doesn't isolate. It flags a bypassed hook but won't stop it. Real sandboxing (Landlock / bubblewrap) is on the roadmap, not done.
  • Web UI and claude must be the same OS user. Otherwise each writes its own ~/.cc-monitor/ and the approvals and audit pages sit empty. There's a card on Home specifically for this.
  • confirm needs /dev/tty. CI and headless environments get a straight deny.

Rough edges

  • Bypass detection is fuzzy matching. Under load, with the probe lagging, verify may take a moment to show the latest results.
  • Byte counts are TCP only. Both IPv4 and IPv6 TCP carry upload/download byte counts; UDP appears on the connection timeline with no byte counts.
  • Location accuracy is whatever the free GeoIP database gives you. Mobile and CDN egress IPs often land on a data centre rather than the user. That's IP geolocation, not something I can fix.
  • The audit log runs as the same user as the monitored process. In theory a same-user process could delete or alter it. Remote forwarding, append-only attributes and the like aren't done.
  • The probe isn't a daemon. Started by hand with sudo; no systemd unit, no autostart.
  • Not a single-file binary. Runs on the system Python; PyInstaller / Nuitka are on the TODO list.
A personal open-source project under MIT, provided as-is with no warranty. Work interrupted by a false block, an incident a rule missed, probe permission trouble, or something you broke editing rules yourself: not my responsibility. Run it in a non-production environment first and read what the default rules block. Which third-party modules it pulls in and where your data actually lives are covered in SECURITY.md.

Give it a go. If it's not for you, make uninstall

First thing after installing, run CC-Monitor rules and see what the defaults block. If something looks off, open an issue.

$ git clone https://github.com/cn0xroot/CC-Monitor.git && cd CC-Monitor && ./install.sh
Source on GitHub