简体中文 | English
Monitoring target: every operation Claude Code performs on this machine that could affect system or data security — file reads/writes, command execution, network access, and anything else a Claude Code tool can trigger.
Goals:
- Visibility: record what Claude Code did (who, when, on what, what action, what result).
- Control: high-risk operations can be blocked/alerted/require human confirmation, not just audited after the fact.
- Hard to bypass: the monitoring capability must not be easy to disable by Claude Code itself (whether through normal behavior or after being hijacked by prompt injection).
- Cross-platform: works on both Linux and macOS, sharing as much core logic as possible.
Claude Code's operations fall into four categories:
| Category | Specific operations | Corresponding Claude Code tools |
|---|---|---|
| File I/O | Read files, write/overwrite files, edit (diff), batch replace | Read / Write / Edit / NotebookEdit |
| Command execution | Shell commands, background processes, scripts | Bash |
| Network access | Fetch web pages, call external APIs/MCP services | WebFetch / WebSearch / MCP tools |
| Meta operations | Spawn subagents, cross-session messages, permission mode switches | Agent / SendMessage / permission mode changes |
Two outcomes need to be guarded against above all: damaging the system (deleting files, changing config, running dangerous commands) and leaking data (reading credentials/secrets and exfiltrating them over the network).
Relying solely on Claude Code reporting on itself (hooks) has a fundamental problem: if the Claude Code process is bypassed, the hooks config is tampered with, or an attacker disables monitoring through some other path (e.g. tricking the user into editing the config directly), monitoring becomes worthless. The design therefore uses two lines of defense — application layer + system layer:
-
Application layer (primary, semantically rich): uses Claude Code's built-in Hooks mechanism (
PreToolUse/PostToolUse, etc.) to intercept before and after every tool call, getting structured information (tool name, arguments, cwd, session_id). The advantage is clear semantics ("Bash ranrm -rf /tmp/x") at low cost (a handful of cross-platform shell/Python scripts). The downside is that this layer runs inside the monitored subject's own framework, and can in theory be bypassed by tampering with its configuration. -
System layer (backstop, anti-bypass): audits/sandboxes the Claude Code process tree at the OS level, independent of whether Claude Code cooperates. Even if the application layer is bypassed, the system layer still sees the real
execve/open/connectsyscalls, serving as the last line of defense.
Both layers report independently to the same local agent and cross-check each other (e.g. if the system layer observes a file write but the application layer has no corresponding event, that mismatch is itself a high-risk signal that monitoring has been bypassed).
flowchart TB
subgraph App["Application layer (semantic layer)"]
CC["Claude Code process"] -- PreToolUse/PostToolUse --> Hook["Hook script"]
end
subgraph OS["System layer (backstop)"]
CC -- real syscalls --> Kernel["Kernel: execve/open/connect"]
Kernel --> Probe["OS probe\n(eBPF / auditd / ESF)"]
end
Hook --> Agent["Local monitoring agent (persistent)"]
Probe --> Agent
Agent --> Policy["Policy engine\n(rule matching / risk grading)"]
Policy -->|allow/log| Log["Audit log\n(local storage + optional upload)"]
Policy -->|high risk| Block["Block / confirm dialog / alert"]
Block -. exit code 2 .-> CC
- Registers
PreToolUseandPostToolUsehooks in~/.claude/settings.json(global) or a project's.claude/settings.json, with a matcher coveringBash|Write|Edit|Read|WebFetch|.... - The stdin JSON the hook receives includes
session_id,cwd,tool_name,tool_input(e.g. the raw command text / file path). - Implemented as Python or a compiled Go binary (cross-platform, no dependencies), forwarding events to the local agent (Unix domain socket — low latency, permissions controllable).
- If a
PreToolUsehook judges an operation to be high-risk, it denies it with a specific exit code; Claude Code aborts that tool call and reports the denial reason back to the model.
| Capability | Linux approach | macOS approach |
|---|---|---|
Process event auditing (execve-level bypass detection) |
Implemented: a bpftrace script (cc_monitor/probe_linux.bt) tracks execve/connect across the entire descendant process tree spawned by the claude process, without depending on auditd |
Endpoint Security Framework (eslogger allows quick validation without writing a custom System Extension; a production build needs a signed ES client + user-granted Full Disk Access) — not yet implemented; this layer underlies CC-Monitor verify's bypass detection, which remains Linux-only |
| Mandatory sandbox (blocking, not just auditing) | Landlock LSM (kernel ≥5.13, restricts reads/writes by path) or wrapping the process with bubblewrap/firejail, restricting writable directories and mounting a read-only root — not yet implemented | sandbox-exec (with a custom profile) or running inside a container/lightweight VM (OrbStack/Docker Desktop) — not yet implemented |
| Network monitoring | Implemented: captures the destination IP:port of connect() syscalls directly via eBPF (+ a uprobe:libc:getaddrinfo that records the hostname the moment the application resolves it, reverse DNS as a fallback), plus tcp_sendmsg/tcp_cleanup_rbuf kernel probes for upload/download byte counts — no TLS termination, no CA certificate needed; the Web UI has a dedicated Network tab with a detail table, IP geolocation (local MaxMind/DB-IP Lite database), and a WebGL2 world map |
Implemented (cc_monitor/probe_darwin.py): samples the claude process tree's connections every 2s with the built-in nettop, recording destination IP:port and upload/download byte deltas into the same network_traffic table as Linux — no root required; unlike the Linux version it has no getaddrinfo hostname capture (falls back to reverse DNS) and no execve observation (the bypass detection in the row above doesn't apply on macOS) |
How the process tree is identified: after a child process forks but before it actually execs
a new program, its comm hasn't changed yet — it still inherits the parent's ("claude"). The
moment that child execve's into something else is evidence it descends from Claude Code;
sched_process_fork propagates this "lineage" to every subsequent descendant. In practice this
cleanly separates Claude Code's own activity from the noise of other desktop processes (conky,
gnome-shell, etc.).
Cross-verification: CC-Monitor-probe fuzzy-matches the top-level shell invocations it observes
(things like bash -c "...") against recent hook_pre Bash records (time window + substring
match after stripping quotes, since Claude Code's Bash tool often wraps the original command in a
eval '<command>' shell-snapshot script before executing it). Anything that doesn't match gets
flagged as hook_bypass_suspected — the typical scenario being "the approved command itself was
short, but at runtime it went on to exec something that was never reviewed at all." Inspect these
with CC-Monitor verify.
On Linux, requires root to run (bpftrace needs CAP_BPF/CAP_PERFMON or plain root); the macOS
nettop network probe does not require root. Both platforms are currently started manually,
not yet packaged as an autostart persistent service (whether to install one as a systemd/launchd
service is an operational decision — during the MVP phase it's run manually).
Recommendation for the MVP phase: have the system layer do auditing only at first (no mandatory blocking — low cost, no kernel extension/signing needed); file and command blocking is handled primarily by the application-layer hooks; the network layer is managed uniformly via a local proxy (this approach is identical on Linux/macOS and offers the best cost/benefit ratio). The system layer's mandatory sandbox (Landlock/sandbox-exec/containerization) is an optional "high-security mode" reserved for Phase 2.
Example rules (configurable, stored as YAML/JSON):
- High risk (deny + alert by default):
rm -rf,dd,chmod -R 777 /,curl|bash,sudo/su/pkexecprivilege escalation, writing to~/.ssh/*,.env,~/.aws/credentials, accessing a non-whitelisted domain combined with reading a sensitive file (a read + exfiltrate combination triggers higher risk), attempting tokill/pkillthe monitoring's own probe/hook process (a concrete rule-level instance of the "two layers cross-check each other" idea from §3 — the act itself is a signal that bypass is being attempted), and database clients (mysql/psql/redis-cli/mongo/sqlite3, etc.) runningDROP/DELETE/TRUNCATE/FLUSHALLdirectly (the same risk category as "deleting files," but entirely outside the filesystem monitoring's field of view), and reverse-shell/backdoor execution (the-e/-cvariants ofnc/ncat/netcat,socat exec:, and amkfifo-plus-named-pipe reverse shell — coverage keeps expanding here since the sneaky ways to write one of these keep multiplying), tampering with Claude Code's own config (~/.claude/settings.json,.claude/hooks/,CLAUDE.md— ifkill_monitoring_processis the process-layer version of "kill the monitoring to bypass it," this is the config-layer version; rewriting the config to drop a hook registration is stealthier than killing a process, since no process has to die for the application-layer monitoring to go silently dark on the next tool call), Docker-socket-mount container escapes (-v /var/run/docker.sock:..., a more common escape technique than--privileged/-v /:/), private-key headers or fixed-prefix secret formats (AWS/GitHub/Anthropic, etc.) appearing in written content (no longer requiring the file name itself to look sensitive), and git-hooks/git-config persistence (core.hooksPath,url.insteadOf— the git-ecosystem counterpart to the same "plant a persistent backdoor" risk category ascrontab_persistence/systemd_persistence). - Medium risk (log + confirmation dialog): writes outside the project directory, modifying
system config files (
/etc/*), bulk file deletion. - Low risk (log only): routine reads/writes inside the project directory, git operations,
reading shell history files or running a bare
historycommand (command history can retain plaintext credentials typed in the past — a real but lesser risk than reading a key file directly, not high-risk but still worth a trail).
Matching approach: commands are matched with rules + keywords/regex, optionally combined with a lightweight local model for semantic judgment (e.g. having it explain "what is this command block's intent, does it match a known destructive pattern"); file paths use glob allow/deny lists.
- Stored locally as SQLite or append-only JSONL; the agent process's permissions are isolated from the Claude Code process (a different user, or at minimum directory permissions that only allow the agent to write while Claude cannot).
- On Linux, the log file can be marked
chattr +a(append-only) for extra tamper resistance; logs are also forwarded asynchronously to a remote/separate host, to avoid the "logs get wiped too once the local machine is compromised" problem. - Event schema (shared across both layers):
{
"ts": "2026-09-11T22:10:00Z",
"session_id": "...",
"source": "hook|os_probe",
"tool_name": "Bash",
"action": "execute",
"detail": {"command": "rm -rf ./build"},
"cwd": "/home/init3/project",
"risk_level": "high",
"matched_rule": "dangerous_delete",
"decision": "blocked|allowed|pending_confirm"
}- High-risk operations: terminal/system notification (
notify-send/ macOSosascript) + an optional requirement that the user click "allow once / always allow / deny" in the notification or a local web page. - A local CLI (e.g.
CC-Monitor tail,CC-Monitor rules) for live-viewing the event stream and adjusting rules — the MVP phase originally didn't plan a web dashboard, but one got built anyway (webui/) and is now the most feature-complete entry point; see README.md for what it covers. This design doc tracks architecture, not per-page detail.
- MVP (done): hook interceptor + policy engine (rule matching) + local SQLite audit log + CLI
viewer (
CC-Monitor tail/rules/stats). Covers Linux/macOS, both verified: hooks / AI Approvals / usage display / Web Terminal have all been tested and confirmed working on macOS. - Phase 2 (Linux portion done; macOS network portion done):
CC-Monitor-probe(bpftrace on Linux,nettopon macOS) tracks the process tree's exec/connect activity or network connections for audit cross-verification;CC-Monitor verifydetects "application layer bypassed" situations; the network layer capturesconnect()directly instead of a MITM proxy; high-risk operations already get both a web-based confirmation UI (AI Approvals, including mirroring Claude Code's own nativePermissionRequestdialog) and desktop alerts (Electron: Dock bounce + badge + system notification). Still not done:execve-level bypass detection on macOS (Endpoint Security Framework, the first row of the §4.2 table above), and running it as a persistent service on either platform. - Phase 3 (platformization, not started): an optional mandatory sandbox mode (Landlock/bwrap, sandbox-exec/containerization), cross-machine log aggregation, a community-shared rule set.
- The application-layer hooks depend on Claude Code calling them honestly; if
settings.jsonis tampered with (e.g. permission misconfiguration lets another process rewrite it), the hooks can be turned off — this is exactly why the system-layer backstop audit exists. - macOS's mandatory sandbox, and its
execve-level system audit (ESF), both require the user to manually approve them in System Settings (Full Disk Access, System Extension signing), so fully silent deployment isn't possible for those two pieces — neither is implemented yet. This doesn't affect what's already usable on macOS without ESF: hooks, AI Approvals, usage display, the Web Terminal, and the network-layer probe are all independent of it. - Semantic-layer rules can't cover every "looks harmless but actually isn't" command combination — the rule set should keep iterating, with human confirmation kept as a backstop.
- The policy engine currently matches command text with plain
re.searchand has no quote/heredoc awareness — content inside a multi-line quoted argument or a heredoc body could in theory get misread as part of the command itself by a rule (the Web UI side solved the equivalent problem for its command-classification display with a small shell tokenizer,splitShellSegments(), but the policy engine hasn't been brought in line with that yet; this is a pre-existing characteristic shared by every rule, not something introduced by any single new rule).