Harpax watches every Claude Code session through three independent channels, runs each event through a layered detection pipeline, and either blocks the action, flags it, or lets it through — all before the tool call actually executes.
The lifecycle of a tool call
Here's what happens when Claude tries to do something:
- Claude generates a tool call (for example,
Bash: cat ~/.ssh/id_rsa). - Claude Code fires a
PreToolUsehook, which invokes Harpax'shook-clientbinary. - The hook client sends the event over a local IPC socket (default
127.0.0.1:9851) to the Harpax daemon. - The daemon runs the event through the detection pipeline. If a critical or high-severity rule fires, the daemon returns a block verdict.
- The hook client exits with code
2, which Claude Code interprets as "do not execute this tool call." Claude sees the block and continues with other work. - The block appears in the Harpax GUI as a toast. You can Allow Once, Always Allow, or Deny. If you allow it, the action is re-injected into the session through Harpax's MCP server.
All of this happens in under a few hundred milliseconds in the typical case — the daemon enforces a configurable hook timeout (default 2000ms) so a stuck rule never freezes your session.
Three monitoring channels
Channel 1 — Hooks (the enforcement layer)
Claude Code's hook system fires synchronous events before and after every tool call, on every user prompt, and at session start and end. Harpax registers PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, and SessionEnd hooks. Because hooks are synchronous, this is the only channel where Harpax can actually block an action.
Channel 2 — I/O Tee (full conversation visibility)
The shim tees Claude's stdin and stdout, giving Harpax the complete conversation stream. This is where Harpax sees the actual text Claude is producing — reasoning, messages to you, and content that hooks alone wouldn't show.
Channel 3 — Event Stream (lifecycle and cost)
An asynchronous event stream captures token usage, model selection, and session-level metadata. This feeds the cost tracker and the live monitor.
The detection pipeline
Each captured event runs through a tiered pipeline. Tiers are independent — you can enable or disable them in ~/.harpax/config.yaml or the Settings page.
| Tier | Method | Latency | Status |
|---|---|---|---|
| T1 | Regex pattern matching against YAML-defined rules | Microseconds | Active |
| T2 | Vector embedding similarity (ONNX model) | ~10–50ms | Optional |
| T2.5 | Cross-event sequence and state rules (JS) | Milliseconds | Active |
| T3 | LLM review for ambiguous cases | Seconds | Optional |
T1 and T2.5 are always on. T2 and T3 are opt-in because they require additional dependencies (an embedding model on disk, an LLM API key). Detections from each tier carry a confidence score; an escalation coordinator weighs them and produces a final verdict.
Rules: built-in and your own
Built-in rules ship in internal/detection/builtin/ as YAML and cover prompt injection, memory poisoning, and skill threats (data exfiltration, credential access, permission bypass, code execution, persistence). You can write your own rules in JavaScript and drop them in ~/.harpax/rules/; they hot-reload by default. JS rules have access to the current event, session history, and helpers like block(), flag(), and alert().
// Example: block network egress after credential exposure
if (session.has("credential_exposed") &&
event.type === "pre_tool" &&
/curl|wget|Invoke-WebRequest|WebFetch/.test(JSON.stringify(event.parsed))) {
block("network egress blocked — credential exposure in session history");
alert("critical", "Attempted network egress after credential exposure");
}
Active vs. passive mode
Harpax ships in passive mode by default. Detections are logged and surfaced in the GUI, but nothing is blocked — useful for learning what your agent actually does. Flip to active mode in Settings when you're ready to start enforcing.
In active mode, the default mapping is: critical → block, high → warn, medium/low → log. Tighten or loosen these in configs/default.yaml under protection.severity_actions.
Override system
When Harpax blocks an action, the GUI shows an override toast with three choices:
- Allow Once. A 60-second temporary allowlist entry is created and the action is re-injected into the Claude session.
- Always Allow. The pattern is added permanently to
~/.harpax/allowlist.yaml. Future matching actions pass through automatically. - Deny. The block stands.
No decision within 30 seconds (configurable via autoDenyTimeout) means automatic deny. This is intentional: a session that's waiting for human review should not silently approve actions just because you stepped away from the keyboard.
Storage and retention
Sessions are written as JSONL to ~/.harpax/sessions. After a configurable delay (default 5 minutes after session end), JSONL files are compressed and ingested into a local SQLite archive for fast historical queries. Raw JSONL is deleted after the retention period (default 90 days).