Inspiration
Autonomous coding agents—such as Claude Code, OpenAI Codex CLI, and local LLM agents—now operate with direct access to developer terminals, local filesystems, and internal microservices. Yet beneath their productivity lies a critical architectural vulnerability: the Confused Deputy problem.
When an agent reads an untrusted GitHub issue, an inlined @file, a poisoned README.md, or a third-party API response, any hidden instructions inside that data enter the LLM's context window with the exact same authority as the developer's own prompts. Relying on system prompts to tell an LLM "ignore malicious instructions in tool outputs" is fundamentally flawed: probabilistic models cannot enforce deterministic security boundaries.
We were inspired by service-mesh architectures (like Envoy and Istio) to ask: What if AI agents had a local, zero-trust security sidecar that inspected untrusted data and tool executions at the network and process boundary—before the LLM could be poisoned or a rogue command could execute?
What it does
Zero-Trust Agentic Sidecar is a fail-closed Rust security proxy (127.0.0.1:8080) that sits transparently between AI coding agents and the outside world. Verified live with Claude Code, it enforces security across two boundaries without modifying a single line of agent code:
Agent CLI (Claude Code)
├── Route A: POST /v1/messages ──► [Sidecar :8080] ──► Scoped Kev-4B Check ──► Anthropic API
└── Route B: POST /api/execute ──► [Sidecar :8080] ──► JWT + Policy ∥ Kev-4B ──► Signed HMAC Backend
Route A — Transparent LLM Context Gateway (
POST /v1/messages):- Drop-In Claude Code Gateway (
scripts/claude.sh): Routes live Claude Code traffic through the sidecar using either the user's existing Claude OAuth login pass-through or a sidecar-heldANTHROPIC_API_KEY(never exposed to the agent). Preserves request bodies byte-for-byte so extended thinking signatures and Anthropic prompt caching remain intact. - Harness-Aware New-Turn Scoring (
score = "new"): Extracts and scores only newly arrived untrusted content since the model's last turn (tool_resultoutputs,@filecontents inlined by Claude Code, and new user text) paired with the originatingtool_usecall as context. - In-Protocol Block Replies & Stateful Poison Memory: Instead of crashing the CLI with a raw HTTP
403, settingon_block = "message"synthesizes a valid Anthropic assistant response (JSON or streamingtext/event-streamSSE) that Claude Code displays directly in the terminal ([Zero-Trust Sidecar] Blocked...), while remembering the SHA-256 hash of the poisoned content (remember_blocked = true) until the user runs/rewindor/clear. - Native Desktop Alerts: Fires rate-limited native OS notifications on macOS (
osascript) and Linux (notify-send) whenever an injection is blocked.
- Drop-In Claude Code Gateway (
Route B — Execution & Local Tool Firewall (
POST /api/execute):- Intercepts both local CLI tool calls (
bash,file_edit,web_fetch) and outbound microservice requests (GET/POST/DELETE). - Evaluates Deterministic Policy Rules (JWT authentication, RBAC roles, linear-time regex filters, path restrictions, and host allowlists) in parallel with Local Kev-4B Semantic Intent Analysis.
- Cryptographically signs approved requests so downstream microservices reject any traffic attempting to bypass the sidecar.
- Intercepts both local CLI tool calls (
How we built it
1. Rust & Axum Fail-Closed Proxy
We built the core proxy in Rust using axum, tokio, and reqwest. Every route is engineered to fail closed: missing secrets, expired JWTs, malformed payloads, or model timeouts ($\Delta t > \tau_{\text{timeout}}$) immediately result in a deterministic deny rather than an unsafe fallback.
2. Local Offline ML Inference (Kev-4B / Kev-0.8B)
To ensure zero data leakage to third-party security APIs, we run bellesecurity/Kev-4B (and Kev-0.8B for 16 GB machines) locally via MLX on Apple Silicon and vLLM on CUDA. Model weights are pinned by commit and verified against SHA-256 hashes in models.lock.json before loading.
For any inspected input $x$, the Kev engine outputs a calibrated threat vector:
$$\mathbf{v}(x) = \begin{bmatrix} P_{\text{inject}}(x) \ P_{\text{intent}}(x) \end{bmatrix} \in [0, 1]^2$$
When untrusted tool outputs exceed the single-pass token budget, the sidecar partitions $x$ into $K$ overlapping sliding windows ${w_1, w_2, \dots, w_K}$ and applies conservative max-pooling across windows so an injection hidden in the middle of a large file cannot be diluted by surrounding benign text:
$$P_{\text{inject}}(x) = \max_{k \in {1, \dots, K}} P_{\text{inject}}(w_k), \quad P_{\text{intent}}(x) = \max_{k \in {1, \dots, K}} P_{\text{intent}}(w_k)$$
A request is permitted if and only if both deterministic policy $\mathcal{D}(x)$ and the dual ML thresholds $(\tau_{\text{inject}}, \tau_{\text{intent}})$ are satisfied:
$$\text{Decision}(x) = \mathcal{D}(x) \;\land\; \Big(P_{\text{inject}}(x) < \tau_{\text{inject}}\Big) \;\land\; \Big(P_{\text{intent}}(x) < \tau_{\text{intent}}\Big)$$
3. Concurrent Evaluation & Hot-Reloading Live Policy (LivePolicy)
On Route B, deterministic RBAC/regex inspection and local ML inference execute concurrently via tokio::join!, bounding total inspection overhead by the slower branch rather than their sum:
$$T_{\text{sidecar}} = \max\left(T_{\text{policy}},\; T_{\text{Kev}}\right) \le \tau_{\text{timeout}}$$
All thresholds, endpoint rules, local tool rules, and CLI environment mappings (ANTHROPIC_BASE_URL, OPENAI_BASE_URL, OLLAMA_HOST) live in policy.toml / policy.local.toml. Our LivePolicy watcher reloads file edits within $1\text{ s}$ with zero downtime—and if an edit contains invalid TOML or an unclosed regex, it rejects the edit and preserves the last known-good policy state $\mathcal{P}_{t-1}$.
4. Two-Legged Cryptographic Choke Point
To prevent a compromised agent from calling backend microservices (127.0.0.1:5001) directly and bypassing the sidecar, the proxy attaches a timestamped HMAC-SHA256 signature to every forwarded Route B payload:
$$\sigma = \operatorname{HMAC-SHA256}\Big(K_{\text{sidecar}},\; \text{ts} \mathbin{\Vert} \text{"."} \mathbin{\Vert} \text{body}\Big)$$
Our Python/Flask reference backend verifies both constant-time signature equality and freshness ($|t_{\text{now}} - \text{ts}| \le 30\text{ s}$), rejecting unsigned, tampered, or replayed requests.
Challenges we ran into
- System Prompt Noise & $O(N^2)$ Context Growth in Claude Code: Real agent CLIs resend the entire conversation history plus a ~15,000-token system prompt on every turn. Naively scoring the full request on turn $N$ scales cumulatively as:
$$C_{\text{naive}}(N) = \sum_{i=1}^{N} \left(|S_{\text{sys}}| + i \cdot \bar{L}_{\text{turn}}\right) = \mathcal{O}(N^2)$$
Worse, Claude Code's system prompt naturally contains imperative phrases (e.g., "execute bash commands", "never ignore...") that skew injection classifiers and cause timeouts. We solved this by building Turn-Scoped Delta Extraction (src/turn.rs), which walks backward to the last assistant turn boundary and extracts only the new delta $\Delta_N$, reducing per-turn scoring work to $\mathcal{O}(|\Delta_N|)$.
Disentangling Harness
<system-reminder>Tags from Inlined@fileReads: Claude Code injects<system-reminder>blocks into user messages, but also inlines@filereads inside those blocks (Result of calling the Read tool). If we blindly stripped all<system-reminder>blocks, an attacker could poison an@fileundetected; if we allowed<system-reminder>skipping insidetool_resultblocks, an attacker could wrap a payload in fake<system-reminder>tags. We solved this by applyingskip_patternsstrictly to user text while detecting inlined tool outputs viainline_tool_output_patternsand scoring all tool outputs raw.Preventing Re-Sent Poisoned History: When a turn is blocked via an in-protocol assistant message (
on_block = "message"), the agent's next request still carries the blockedtool_resultin its message history. We implemented stateful SHA-256 content fingerprinting (remember_blocked = true) so any conversation still carrying a previously blocked untrusted payload remains locked until the developer runs/rewindor/clear.
What we learned
- Probabilistic vs. Deterministic Security: LLMs cannot reliably police themselves inside a shared context window. Combining a fine-tuned local classifier (
Kev-4B) for semantic ambiguity with Rust's deterministic policy engine for hard invariants gives the best of both worlds. - Protocol Empathy Matters: A security proxy that breaks prompt caching, strips
anthropic-*beta headers, or crashes the CLI TUI with raw HTTP errors will be turned off by developers. Returning blocks in-protocol (JSON and streaming SSE) and offering a non-blockingmode = "monitor"for threshold calibration made the sidecar practical for daily use. - Adversarial Testing is Essential: Building 700+ automated Rust and Python tests—spanning Unicode homoglyphs, null-byte injections, oversized payload DoS, JWT tampering, and a full Claude Code gateway recording harness—caught subtle edge cases long before live demos.
What's next for Zero-Trust Agentic Sidecar
- Native Menu-Bar Tray App (
GET /v1/alerts): Streaming real-time block events to a desktop UI with a cryptographic "Allow Once" human-in-the-loop override. - Multi-Agent Protocol Adapters: Extending turn-scoped delta extraction and in-protocol block synthesis to the OpenAI Responses API (Codex CLI), Gemini CLI, and local Ollama/vLLM loops.
- In-Process Rust
candleInference: Completing our native Rustcandlebackend (Kev-4BBF16 on Metal and CUDA) to ship a single static binary with zero Python runtime dependencies.