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
  1. 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-held ANTHROPIC_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_result outputs, @file contents inlined by Claude Code, and new user text) paired with the originating tool_use call as context.
    • In-Protocol Block Replies & Stateful Poison Memory: Instead of crashing the CLI with a raw HTTP 403, setting on_block = "message" synthesizes a valid Anthropic assistant response (JSON or streaming text/event-stream SSE) 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 /rewind or /clear.
    • Native Desktop Alerts: Fires rate-limited native OS notifications on macOS (osascript) and Linux (notify-send) whenever an injection is blocked.
  2. 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.

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

  1. 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|)$.

  1. Disentangling Harness <system-reminder> Tags from Inlined @file Reads: Claude Code injects <system-reminder> blocks into user messages, but also inlines @file reads inside those blocks (Result of calling the Read tool). If we blindly stripped all <system-reminder> blocks, an attacker could poison an @file undetected; if we allowed <system-reminder> skipping inside tool_result blocks, an attacker could wrap a payload in fake <system-reminder> tags. We solved this by applying skip_patterns strictly to user text while detecting inlined tool outputs via inline_tool_output_patterns and scoring all tool outputs raw.

  2. 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 blocked tool_result in 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 /rewind or /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-blocking mode = "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 candle Inference: Completing our native Rust candle backend (Kev-4B BF16 on Metal and CUDA) to ship a single static binary with zero Python runtime dependencies.

Built With

Share this project:

Updates

Submission history