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 code126signals policy denial specifically. Every decision, block or allow, produces acommand_decisionsecurity 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/.envas whole path segments, not substrings, and is not policy-overridable — a hard minimum beneath whatever the policy additionally protects. Policy-addedprotected_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_statusevents 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_inspectedevent. - 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.