Interception & Proxying

Four integration surfaces carry the enforcement decision to where a developer or an AI agent actually touches sensitive data: the interactive shell, the MCP transport, the system clipboard, and the local Git working tree/history. Each is covered below with what is actually implemented.

Shell interception (PTY)

The protected entry point is a shell function, tcg_shell, generated for zsh, bash, PowerShell 5.1+, and cmd.exe by a hook generator built into the compiled agent — every value it emits is quoted correctly for its target shell, and an unsupported shell produces an explicit error.

%%{init: {"theme": "base", "themeVariables": {"fontFamily": "-apple-system, Segoe UI, Helvetica, Arial, sans-serif", "primaryColor": "#eef2f7", "primaryTextColor": "#0b0b0b", "primaryBorderColor": "#5b6b7c", "lineColor": "#5b6b7c", "secondaryColor": "#eef4ff", "tertiaryColor": "#f4f3f0", "clusterBkg": "#f9f9f7", "clusterBorder": "#c3c2b7", "edgeLabelBackground": "#fcfcfb", "actorBkg": "#eef2f7", "actorBorder": "#5b6b7c", "actorTextColor": "#0b0b0b", "actorLineColor": "#5b6b7c", "signalColor": "#52514e", "signalTextColor": "#0b0b0b", "labelBoxBkgColor": "#eef4ff", "labelBoxBorderColor": "#5b6b7c", "labelTextColor": "#0b0b0b", "loopTextColor": "#0b0b0b", "noteBkgColor": "#fdf3d9", "noteBorderColor": "#c98500", "noteTextColor": "#3d2f00", "activationBorderColor": "#5b6b7c", "activationBkgColor": "#eef4ff", "sequenceNumberColor": "#ffffff"}}}%% sequenceDiagram participant Dev as Developer / AI agent participant Hook as Shell hook (tcg_shell) participant Agent as station-agent (PTY) participant Policy as Policy engine participant Mask as Masking stream Dev->>Hook: command line Hook->>Agent: shell-run-line Agent->>Policy: evaluate(command, snapshot) alt blocked Policy-->>Agent: block Agent-->>Dev: exit code 126, no execution Agent->>Agent: emit command_decision event else allowed Policy-->>Agent: allow Agent->>Agent: execute in PTY, forward stdin, propagate resize Agent->>Mask: stream stdout/stderr Mask-->>Dev: masked output, streamed live Agent->>Agent: emit command_decision event end

Key properties:

  • Pre-execution evaluation. A blocked command never runs — the decision happens before exec. Exit code 126 signals policy denial specifically. Every decision, block or allow, produces a command_decision security event carrying the policy snapshot ID and fingerprint in force.
  • Streaming output. Output is masked through the streaming engine described in Masking & Streaming Enforcement — the terminal stays responsive, with output delivered continuously as it's produced.
  • A recursion guard (an environment marker) ensures a nested mediated invocation executes once.
  • A lightweight, separate hook path specifically gates the launch of known AI coding agents (claude, codex, qwen, qwen-code, cursor-agent, cursor) — this is what triggers the repository pre-launch scan described below, and is intentionally distinct from the broader file/content alias hook so that a permitted agent launch keeps a normal, unwrapped interactive TTY.

Platform-specific coverage:

  • bash/zsh: full interactive PTY mediation exists as a capability — stdin forwarding, terminal resize propagation, and terminal-state restoration on exit (including on error).
  • PowerShell (5.1+): mediated via station-agent's Windows shell-run-line path (powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command ) — the decision, masking, and audit logic execute in the compiled agent, which is the trust boundary; PowerShell is only the execution vehicle for an already-decided command.
  • cmd.exe: mediation is advisory/macro-scoped via a fixed DOSKEY macro list for a defined set of first-word commands.

Why the Windows shell design looks the way it does: four alternatives were considered — PowerShell-only profile functions (doesn't cover cmd.exe at all), a full ConPTY-backed resident shell (materially larger scope, not required to prove the mediation model), ETW/service-only interception (insufficient in isolation), and a third-party terminal wrapper (rejected outright as an unacceptable dependency). The accepted design keeps the compiled agent as the trusted boundary while treating shell-specific scripts as orchestration only.

MCP proxy (Model Context Protocol)

The MCP proxy sits between an AI agent and a local, stdio-based MCP server — transparently relaying the child process's stdin/stdout while inspecting traffic in both directions.

%%{init: {"theme": "base", "themeVariables": {"fontFamily": "-apple-system, Segoe UI, Helvetica, Arial, sans-serif", "primaryColor": "#eef2f7", "primaryTextColor": "#0b0b0b", "primaryBorderColor": "#5b6b7c", "lineColor": "#5b6b7c", "secondaryColor": "#eef4ff", "tertiaryColor": "#f4f3f0", "clusterBkg": "#f9f9f7", "clusterBorder": "#c3c2b7", "edgeLabelBackground": "#fcfcfb", "actorBkg": "#eef2f7", "actorBorder": "#5b6b7c", "actorTextColor": "#0b0b0b", "actorLineColor": "#5b6b7c", "signalColor": "#52514e", "signalTextColor": "#0b0b0b", "labelBoxBkgColor": "#eef4ff", "labelBoxBorderColor": "#5b6b7c", "labelTextColor": "#0b0b0b", "loopTextColor": "#0b0b0b", "noteBkgColor": "#fdf3d9", "noteBorderColor": "#c98500", "noteTextColor": "#3d2f00", "activationBorderColor": "#5b6b7c", "activationBkgColor": "#eef4ff", "sequenceNumberColor": "#ffffff"}}}%% flowchart LR Agent["AI agent"] <-->|"JSON-RPC over stdio"| Proxy["station-agent\nMCP proxy"] Proxy <-->|"JSON-RPC over stdio"| MCPServer["Local MCP server"] Proxy --> Guard1{"Request references\na protected resource?"} Guard1 -- yes --> Block["Block request,\nemit mcp_request_blocked"] Guard1 -- no --> Pass1["Forward request"] Proxy --> Guard2{"Response contains\nsensitive value?"} Guard2 -- yes --> Redact["Redact, re-validate\nas JSON-RPC, emit\nmcp_response_masked"] Guard2 -- no --> Pass2["Forward response"]
  • Request inspection: every string in a request — the method name and all nested parameters — is inspected. Percent-encoded values are decoded up to three rounds and backslashes are normalized to forward slashes before any matching happens. Two distinct matching rules apply depending on the source of the protected value, and they are not the same rule: the built-in floor (.env, .ssh, id_rsa, id_ed25519) matches .ssh/.env as whole path segments, not substrings, and is not policy-overridable — a hard minimum beneath whatever the policy additionally protects. Policy-added protected_resources, by contrast, match by case-insensitive basename equality or substring after normalization — a looser rule than the built-in floor's whole-segment matching. Absolute local paths additionally have symlinks resolved before matching.
  • Response inspection: scanned with the same detector set used elsewhere, plus a base64-region decode-and-scan pass (a common encoding for tool-call payloads). A redaction that would corrupt the JSON-RPC structure is treated as an error and never emitted — the proxy will not deliver a broken protocol frame in the name of safety.
  • Session events: MCP session start/stop produce mcp_session_status events for operator visibility.

Clipboard monitoring

A resident monitor polls the system clipboard (native tooling per platform — pbpaste/ pbcopy on macOS, PowerShell Get-Clipboard/Set-Clipboard on Windows, xclip on Linux — content is always passed via stdio, never assembled into a shell command line). On detecting a change, it hashes the content and acts only on that change; a masked overwrite's own hash is recorded so the monitor doesn't re-trigger on its own write.

  • Polling interval: 500 ms.
  • Inspection cap: 256 KB, truncated on a UTF-8 boundary. Content over the cap produces a clipboard_partially_inspected event.
  • Masking is destructive — the original clipboard content is not preserved once overwritten.
  • The masking event itself records only hashes and lengths, never the clipboard content.

Local repository scanning

A worker-pool scanner inspects Git repositories entirely on the workstation — source content never leaves the machine; only finding types, detector names, and counts are reported. History scanning walks the full commit graph (every blob reachable from every ref, deduplicated by object hash, bounded by a configurable object budget) — so a secret committed in one commit and removed in a later commit still on a live branch is still found.

The scanner is wired into the AI-agent launch gate described above: before a recognized AI coding agent starts inside a Git repository, a working-tree-plus-history scan runs (cached by repository state and policy identity), and only redacted metadata is persisted — repository-root hash, the launched agent's command, scan status, and file/finding counts, never source content or matched values. One recorded run (2026-08-07, 40 repositories, 1,600 files, full history included) completed in 0.339 seconds, correctly finding a secret from an earlier, still-reachable commit.

Continue to Detection Engine.

results matching ""

    No results matching ""