agentjail
September 3, 2026 · View on GitHub
agentjail is a policy-guardrail layer for AI coding agents (Claude Code, Codex CLI, Cursor). It intercepts every tool call before it executes and evaluates it against OPA-based Rego policies, returning an allow, deny, or ask decision to the agent. No proxy, no wrapper binary, no dynamic-library injection — just hooks and a warm policy daemon.
How It Works
Every major coding agent ships a hook system that fires a command before each tool call — before a file is written, before a shell command runs, before an MCP server is contacted. agentjail installs a hook (agentjail-hook) that forwards the tool-call payload over a Unix socket to a persistent background daemon (agentjail-daemon). The daemon holds the OPA engine warm and evaluates Rego rules in under 5 ms.
coding agent (Claude Code / Codex / Cursor)
|
| fires hook on every tool use (PreToolUse / beforeShellExecution)
↓
agentjail-hook (tiny binary, ~1 ms overhead)
|
| forwards JSON payload over Unix socket
↓
agentjail-daemon (persistent process, holds OPA engine warm)
|
| evaluates Rego rules against the input
↓
allow / deny / ask → returned to the coding agent
The daemon is started on login (launchd plist on macOS, systemd user service on Linux) so OPA cold-start cost (~50 ms) is incurred once at startup. Per-decision latency target is <5 ms.
Hook wire format (Claude Code example)
// stdin to agentjail-hook
{
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": { "command": "rm -rf /tmp/foo" },
"session_id": "...",
"cwd": "/Users/dev/project"
}
// stdout from agentjail-hook
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "rm -rf on paths outside project dir is blocked by policy"
}
}
Exit code 2 + stderr blocks immediately without requiring JSON output.
Fail-open guarantee
The hook never blocks a coding agent when agentjail-daemon is unreachable.
agentjail-hook connects to the daemon's Unix socket with a 30ms timeout; if
the socket is missing, the daemon isn't running, or it doesn't respond in
time, the tool call is allowed. This keeps a daemon crash or restart from
freezing every agent on the box. The OS-level shield is independent of this
path and keeps enforcing filesystem/network restrictions regardless of daemon
state, so a fail-open hook decision doesn't relax sandbox enforcement. Every
fail-open event is captured via telemetry so degraded daemon availability is
visible rather than silent. This guarantee does not extend to credential
paths: the shield's OS-level deny on sensitive paths (~/.ssh, ~/.aws,
etc.) is enforced by the sandbox itself, not the hook, so it holds even when
the daemon is down.
Hook Integration per Platform
| Platform | Hook event | Config file | What is intercepted |
|---|---|---|---|
| Claude Code | PreToolUse | ~/.claude/settings.json | Bash, Write, Edit, Read, all MCP tools |
| Codex CLI | SessionStart + PreToolUse + PermissionRequest + PostToolUse + Stop | ~/.codex/hooks.json | Lifecycle attestation; Bash, apply_patch, MCP tools |
| Cursor | beforeShellExecution + beforeMCPExecution + beforeReadFile | ~/.cursor/hooks.json | Shell, MCP, and agent file reads |
The hook is configured in a file the agent reads at startup. Because the agent runs inside the hook framework, it cannot remove the hook from within itself. The policy binary runs in the host shell, not in the agent's process.
Codex native approval bridge
Codex's PreToolUse hook cannot create a native approval prompt. For a
canonical AgentJail ask on a Codex Bash call, the daemon therefore mints an
in-memory, one-use approval challenge and the hook rewrites only that tool input
to AgentJail's managed approval broker. Codex's exact managed execpolicy rule
then creates its native prompt. PermissionRequest binds the challenge to the
same session, turn, and working directory and records a fresh process-start
boundary; it deliberately leaves the native decision to Codex.
On approval, the broker asks the daemon to redeem the challenge. Redemption is
one-use and requires the active Codex session plus a verifiable, fresh descendant
process chain from the recorded agent process. The daemon returns the original
command and working directory only after those checks; the broker execs that
exact command. Cancel, expiry, replay, a later tool-call epoch, a missing
managed rule, --ignore-rules, approval_policy=never, a daemon restart, or
any unverifiable process topology fails closed. The standard hook availability
posture remains fail-open for ordinary policy evaluation. Approval-capable
Codex PreToolUse requests are the exception: they deny when the daemon
response is unavailable because the hook cannot yet distinguish a canonical
allow from an approval-requiring ask. A broker invocation likewise cannot
execute while its authorization service is unavailable.
The broker command contains an opaque challenge rather than the original shell
text. AgentJail uses Codex's supported systemMessage field to show the redacted
effective shell command immediately before the native prompt, while the fixed
command inside Codex's prompt identifies only the shell-command approval
operation. The transport follows every effective Codex Bash ask, including
user-authored custom rules, rather than enumerating policy rule IDs. Non-Bash
Codex ask decisions retain their fail-closed adapter behavior because Codex
cannot initiate an equivalent prompt for those tools. The normal redacted
PreToolUse decision record remains unchanged; the bridge adds no original
command text to its audit records or structured logs.
The opaque, one-use challenge is necessarily visible in the short-lived broker
argv and its daemon socket request so concurrent prompts can be correlated; it
is never logged or audited and cannot redeem without the bound session, epoch,
and fresh process-topology checks. Same-user process-list exposure is therefore
a bounded limitation, not an authorization channel. See
ADR 0118-codex-approval-broker and
ADR 0119-command-approval-transport.
Policy Model
Policies live under agentpolicy/policies/ (mirrored into the binary at
cmd/agentjail/policies/) and are written in Rego.
Candidate → resolver → decision
Every policy file contributes candidate entries to the partial set
data.agentjail.candidate. resolver.rego is the sole producer of
data.agentjail.decision: it derives an effective_candidate set and picks the
most restrictive (deny > ask > allow; lowest rule_id breaks ties). When nothing
fires, the default is ask (fail-safe, not silent allow). Every rule has a
namespaced rule_id (file_policy/…, command_policy/…, mcp_policy/…,
library/…, custom/<name>/…).
For Bash calls, the daemon parses executable invocations before OPA evaluation
and supplies both binary names and typed command intents. Git remote-update
policy classifies the subcommand after documented global options such as -C
and -c; it does not scan the whole shell string for adjacent words. This
distinguishes an executed operation from inert arguments in commands such as
source searches. See ADR 0118-codex-approval-broker.
Config overlay (ADR 0012)
The daemon loads ~/.agentjail/policy.yaml, merges it over built-in defaults,
and injects it into OPA as data.agentjail.config (re-injected on reload —
a control message over the daemon socket, with SIGHUP retained as a
fallback; see ADR 0052 —
decision cache invalidated). Rego reads config from there — e.g.
data.agentjail.config.mcp.allowed, .file.temp_roots, .disabled_rules.
Request paths and cwd are canonicalized (symlinks/.. resolved) at ingest, so
policies always see real absolute paths; cwd is part of the decision-cache key.
file_policy.rego — sensitive path enforcement (ADR 0013)
Two tiers:
is_protected_credential→ hard deny everywhere (regardless of cwd):~/.ssh,~/.aws,~/.gnupg,~/.config,~/Downloads,~/Desktop,~/.npmrc/~/.pypirc/~/.git-credentials/~/.docker/config.json/~/.kube/config/~/.cargo/credentials,~/Library/Keychains,/etc, and non-temp/var.is_sensitive_basename(.env*,credentials*,secrets*,*.pem/.key/…,id_rsa-family) → ask when inside the granted project dir, deny outside. The.envpredicate is op-aware: reads use the broad.env*match above unchanged, writes/edits use a narrower secret-form deny-list only (.env,.env.local,.env.production, ...) so non-secret templates like.env.exampleare writable - see ADR 0057.
The temp tree ($TMPDIR, /tmp, /var/folders/…) is allowed. Project
membership is boundary-safe (p == cwd OR startswith(p, cwd + "/")), so a sibling
like /proj2 doesn't match cwd=/proj. Writes to ~/.agentjail/ get their own
locked file_policy/agentjail_self deny (self-protection).
mcp_policy.rego — MCP server allowlist
Allowlist by server name (glob). At install, agentjail seeds the allowlist from
the MCP servers already configured in Claude/Codex/Cursor (trust-on-install),
so existing setups keep working; the default blocklist (*stripe*, *payment*,
…) always takes precedence. Manage with agentjail mcp allow/block/list —
allow/block mutate policy, so they require an interactive-terminal
confirmation (an agent can't self-approve a server).
# ~/.agentjail/policy.yaml
mcp:
allowed: ["claude-mem", "context7", "github*"]
blocked: ["*stripe*", "*payment*"]
web_policy.rego — web read tools (WebSearch / WebFetch)
Coding agents route their read-only web tools through the hook. Without a rule
these hit resolver/default → ask, so every search/fetch prompts the user
(and the agent host's per-domain "don't ask again" can't suppress an agentjail
ask). So agentjail governs them explicitly: WebSearch is always allowed (a
query to the harness's search backend, no arbitrary endpoint), and WebFetch is
allowed by default (read-only GET) unless its target host matches a
configurable blocklist:
# ~/.agentjail/policy.yaml
web:
blocked: ["*tracking*", "*.internal", "169.254.*"] # host globs; default []
Host globs match case-insensitively and * spans dots. This is domain control,
not exfil-proofing — a determined prompt-injected agent could pick an unlisted
host; the bigger exfil vector (Bash curl/POST) stays governed by
command_policy. Users who want WebFetch to prompt again can add
web_policy/fetch to disabled_rules (it falls back to the default ask).
command_policy.rego — dangerous shell patterns
Block or prompt before high-risk patterns: rm -rf outside the project,
curl … | bash, chmod -R 777, sudo, dd, > /dev/disk*,
ssh-keygen, gpg --export-secret-keys, env | curl exfil, and more; ask on
package publish. git force-push is branch-aware: force-pushing the default
branch (main/master) is denied, force-pushing a topic/feature branch is
allowed (normal rebase / PR-update flow), and a bare git push -f (implicit
current branch) asks. An always-on, locked command_policy/no-policy-mutation rule
blocks an agent from running agentjail policy disable/mcp or writing into
~/.agentjail/. The mutation guard uses specific subcommand patterns (e.g.
agentjail\s+update\b) rather than broad keyword matching, to avoid false
positives when "agentjail" or "update" appears as a path component or in prompt
text. The sensitive-path rule likewise ignores only static, non-expanding
git commit -m message text; command substitutions, other arguments, chained
commands, and remote shell payloads remain subject to the deny.
Tuning, disabling, and custom rules (ADR 0014)
- Disable any rule by adding its
rule_id(or apolicy/*glob) todisabled_rulesinpolicy.yaml, or viaagentjail policy disable <rule_id>.resolver.regodrops disabled candidates fromeffective_candidate. - Locked self-protection set — a hardcoded constant in
resolver.rego(file_policy/agentjail_self,command_policy/no-policy-mutation,resolver/*) can never be suppressed bydisabled_rules, so nopolicy.yamledit unlocks it. The CLI also requires--force+ an interactive TTY confirm to disable a core rule, and logs mutations to~/.agentjail/audit.log.library/no-daemon-killis on by default but disableable with--force— the daemon runs under launchd/systemd withKeepAlive=true, so a kill is a speed bump, not a permanent disable. - Custom rules —
agentjail policy add <file.rego>parses the module AST and accepts only partialcandidateentries inpackage agentjail; resolver helpers anddecisionare not extensible. It validates reservedcustom/<name>/<rule>ids and compiles the full bundle before installation. The daemon repeats the extension-surface validation during deterministic quarantine, so a manually dropped unsafe module is skipped rather than weakening the baseline.
See ADRs 0012, 0013, and 0014 for the decisions behind these.
Self-protection model (ADR 0025)
agentjail's self-protection uses a layered enforcement model inspired by EDR architecture — enforcement at the point of effect, not the point of intent:
- Regex rules (Tier 1) are the UX/signals layer: they produce clear deny
messages ("you are trying to disable agentjail policy") and catch obvious
attempts early. They are defense-in-depth, not the primary guarantee.
Write/Edit to hook config files (
~/.claude/settings.json, etc.) produce ask (user can approve legitimate edits); Bash-based writes deny. - Shield (Tier 1.5) is the enforcement layer: Seatbelt/Landlock deny
writes to
~/.agentjail/at the kernel level, regardless of how the write is attempted (python -c,node -e,eval, etc.). - Tier 2 (microVM) makes self-protection structural: the agent runs in a VM, agentjail runs on the host, and the attack surface doesn't exist.
See ADR 0025 for the full mutation surface analysis and design rationale.
OS-native Sandbox (agentjail-shield)
The hook layer is cooperative — the agent must call the hook, and the hook must
pattern-match the command. Shell tricks like variable expansion, eval, or
non-shell interpreters (python -c, osascript) can bypass hook-level
protection. agentjail-shield closes this gap by wrapping the agent in the
operating system's kernel sandbox before exec'ing it. Every subprocess
inherits the restrictions.
agentjail-shield
│
├─ [macOS] generates Seatbelt sbpl profile → sandbox-exec -p <profile> <agent>
├─ [Linux] landlock_create_ruleset + landlock_restrict_self → execve <agent>
└─ [other] warning → exec <agent> (fail-open; hook still active)
macOS (Seatbelt): deny-list based. Denies writes to sensitive paths
(~/.ssh, ~/.aws, ~/.gnupg, etc.), denies reads of credential paths, and
restricts network egress. ~/Library/Keychains (user keychain root) is
granted read+write by default so Claude Code's login/token-refresh flow
works — see ADR 0037;
the hook layer still denies an agent's own direct reads of that path. When
agentjail-netproxy is enabled (opt-in via --netproxy; OFF by default in the
interim -- ADR 0046), the
agent is restricted to localhost-only outbound TCP and all HTTPS traffic flows
through the proxy, which enforces the effective allowlist from policy.yaml.
By default (no --netproxy) egress is port-only (80/443, no per-host filtering)
until the transparent tunnel (planned) supersedes the proxy.
The effective allowlist is three tiers -- non-removable essentials (provider
auth + mcp-proxy.anthropic.com), hosts auto-derived from the MCP servers you
allow (mcp.allowed), then your editable network.allowed_hosts -- computed by
EffectiveAllowedHosts(). A malformed policy.yaml or a failed netproxy make
the shield refuse to launch (fail loud / fail closed) rather than silently
weaken enforcement. See ADR 0040
and ADR 0041;
for the end-to-end walkthrough see docs/FLOW.md.
Linux (Landlock): allowlist-based. Grants read-write to /tmp and the
project CWD, read-only to system directories and $HOME, and denies everything
else. Sensitive subdirectories (~/.ssh, ~/.aws, ~/.gnupg) are never
allowlisted. ~/.agentjail -- agentjail's own enforcement state -- is granted
read-only, so policy.yaml, the SQLite DB, and trusted.yaml stay
unwritable and the agent cannot disable its own guardrail. The sandboxed hook
still reaches daemon.sock with no write grant of any kind: Landlock is a
filesystem LSM and does not mediate AF_UNIX connect() at all (measured --
ADR 0067-control-plane-token-auth, addendum 1).
~/.agentjail/run/daemon-ctl.sock (the grant control socket, ADR 0047) is
deliberately excluded from the write grant -- the agent can file grant requests
through daemon.sock but cannot approve them.
No special privileges required. Both sandbox-exec and Landlock run as the
invoking user — no sudo, no entitlement, no kernel module.
Environment hardening (before exec):
- Strips ambient credentials from the agent's env (configurable blocklist)
- Audits for root, readable credential files, IMDS reachability
agentjail-secretsbroker issues scoped, short-lived credentials viagrant/revokeover Unix socket (AES-256-GCM at rest)
For the full user guide, see docs/SANDBOX.md.
For the decision record, see ADR 0001.
Decision Store (SQLite)
Every policy decision is persisted to ~/.agentjail/agentjail.db (SQLite, WAL
mode). The daemon writes; the CLI, UI, and replay tools read via ReadOnlyStore
(a separate read-only connection that cannot write even if type-asserted).
agentjail-daemon (writer)
│ RecordDecision / RecordAuditEvent
▼
agentjail.db (WAL mode, concurrent readers OK)
▲
│ ListDecisions / CountActionsBySession / ListSessions
agentjail logs / replay / ui (readers via OpenReadOnly)
Schema highlights:
decisionstable:id,ts,session_id,tool_name,action,rule_id,reason,summary,tool_input_redacted,elapsed_us,cwd,agentsessionstable:session_id,agent,start_ts,end_ts,decision_countaudit_eventstable: policy enable/disable/reload mutations- Indexes on
(session_id, ts),ts,action,tool_name,rule_id - Automatic retention cleanup via
Cleanup(maxAge) - Tool input redaction at write time: secret-bearing keys are stripped, and
recognised secret values (bearer tokens, provider keys, PEM blocks, URL
passwords) are stripped wherever they appear — including inside a positional
value such as a Bash
commandstring (ADR 0019, ADR 0084-redact-secret-values)
Filter support: store.Filter supports SessionID (substring), Actions
(case-insensitive OR), Tool (exact), Rule (case-insensitive substring),
AfterID (keyset pagination, direction-aware for ASC/DESC), and Limit (clamped
to [100, 10000]).
Unified Audit Log
The audit_log table captures every significant event across daemon, CLI,
shield, and secrets — policy mutations, session lifecycle, shield activation,
credential grants/revokes, config reloads, and more. It replaces the scattered
audit_events table and flat-file audit.log with a single queryable source.
Components emit audit events through the AuditEmitter interface
(internal/audit/), which decouples event production from storage. Event type
constants live in internal/audit/audit.go. Three durability classes govern
how events are written:
- Fail-closed: policy mutations (
policy.change_requested/policy.changed) — audit must succeed beforeconfig.Save()proceeds. - Transactional: emitted in the same SQLite transaction as the primary data write (e.g. session upsert, tool registration). Audit failure is logged but does not roll back the primary write.
- Best-effort: lifecycle events (daemon startup, shield activation). Fire- and-forget, never blocks the hot path.
Decisions are NOT duplicated into audit_log — they remain in the decisions
table, which is optimized for the per-decision hot path. A unified query layer
provides combined chronological views when needed.
The Detail column is redacted at the store boundary using the same key-pattern
matcher and value-pattern sweep as RedactToolInput, with a 4096-byte cap.
Credential values are never stored — only fingerprints (ADR 0032). The shield opens the
database before sandbox activation so pre-opened file descriptors survive
Landlock/Seatbelt restrictions.
See ADR 0033 for the full decision record.
Cost Analytics
The daemon maintains a typed cost index in the existing AgentJail SQLite store.
It runs once after daemon readiness and then at each next local calendar
midnight. Durable per-file checkpoints make missed invocations harmless and
let later runs read only appended, newline-complete Claude Code and Codex JSONL
records. OpenCode remains an external typed SQLite source. agentjail cost and
the local UI use the singleton store's read-only interface and never scan raw
transcripts on a request. See ADR 0142-incremental-cost-index.
The index retains only source/session identity, project/model attribution,
timestamps, token and request-pricing dimensions, fork lineage, and recorded or
computed cost—never conversation or tool-result content. Normalized lineage
facts outlive the 90-day report window because a new Codex fork can inherit an
older parent's cumulative history. Report rows are grouped by UTC session-start
day so existing whole-session --period semantics remain stable.
The shared per-model summary exposes uncached input, cache-read, cache-write,
and output totals. Pricing includes every category, so CLI and UI consumers do
not present output volume as if it were the complete basis for model cost.
Claude's five-minute and one-hour cache-write totals remain distinct through
the typed report contract. Codex cumulative totals provide displayed usage,
while complete last_token_usage records drive request-level price tiers; an
incomplete request sequence is explicitly marked as a base-rate estimate.
Codex usage deltas retain the active model, and forked transcripts remove
ancestor events by their cumulative usage identity before aggregation.
Model costs are computed offline through Gryph's bundled model-resolution and pricing provider, with source-verified supplemental rates for models newer than Gryph's latest release (ADR 0120-bundled-model-pricing and ADR 0121-current-model-pricing and ADR 0123-supplemental-model-pricing). AgentJail sends no transcript or usage data to Gryph. Supplemental pricing semantics overlay a resolved Gryph base rate so a catalog update cannot silently remove TTL or long-context rules. OpenCode's own non-zero recorded cost takes precedence. Pricing revisions rebuild the derived projection from retained typed facts; request-time reporting reads that projection without source I/O. Missing optional sources are ignored; malformed available sources are reported as warnings without affecting enforcement. Oversized JSONL content records are discarded individually within a fixed memory bound so later usage records in the same transcript remain readable (ADR 0122-transcript-record-recovery). Token-bearing models absent from both offline catalogs produce an explicit pricing-unavailable warning.
Budget alerts come from the global cost section of policy.yaml:
cost:
daily_budget: 25
alert_threshold: 0.8
project_budgets:
"~/Repos/production-api": 10
These values are reporting settings, not OPA policy data. Trusted project
overlays cannot change them. The shared report contract powers the CLI and
GET /api/cost/summary?period=7d&project=<dir>.
Local UI
agentjail ui starts a loopback-only HTTP server backed by the SQLite store.
/api/state— sessions, counters (global), recent events (filtered)/api/session?id=<id>— chronological session replay with filters/api/audit— policy-mutation audit log- Server-side filter query params:
action,tool,rule,limit - Counters (
total_allow/deny/ask) are always global; onlyrecent_eventsand session replay rows are filtered FilteredCountandTotalDecisionsin response for "showing N of M"- Frontend sends filters with 300ms debounce; SSE live events remain client-filtered
--edit-policyopt-in enables policy enable/disable controls (read-only by default)
Isolation Tiers
agentjail is designed across three levels of isolation strength. They are not mutually exclusive — stronger tiers can layer on top of lighter ones.
Tier 1 — Hooks (lightest isolation)
The agent runs normally on the host. agentjail intercepts at the agent's own tool-call boundary using the platform hook system described above. No changes to the host OS, no container, no kernel module required.
Characteristics:
- Zero friction to install; hooks are a first-class feature of all supported agents.
- Policy decisions happen in user space, in the host shell.
- An agent that cooperates with its hook framework cannot bypass this layer from within itself.
- Does not protect against agents that have been modified to skip hook dispatch entirely.
Tier 2 — Container / MicroVM (stronger isolation)
The agent runs inside a microVM. The proposed substrate is Microsandbox (built on libkrun) for the developer-laptop path — macOS (HVF), Linux (KVM), and Windows (WSL2) — with Firecracker retained as the server-fleet backend. The VM boundary enforces egress from network.allowed_hosts and keeps credentials out of the guest; the Tier 1 hook + daemon run inside the VM unchanged. The same OPA policy engine governs both sides of the boundary.
What it adds over Tier 1:
- Hard containment: an agent that attempts to ignore hooks is physically prevented from reaching the host filesystem or network.
- Works for agents that do not support hooks at all.
- Stronger audit trail: every syscall crossing the boundary is logged, not just declared tool calls.
Substrate selection, the two-backend split, and the long-term pros/cons are decided in ADR 0016 (status: Proposed). The libkrun and Firecracker spikes live under
agentjail/research/.
Configured host connectors across the boundary
Runtime approval for a configured host connector is not host reachability. The tree contains fixed-destination probe, synthetic ConnectorID route, verified launch capability, and Linux private AF_UNIX transport primitives, but the runtime MCP hook does not activate them. Its allow response cannot safely bind a raw route or an existing tunnel to grant consumption, expiry, or revocation; production connector use therefore fails closed pending a grant-aware MCP data plane. There is no wildcard listener, source-IP trust, guest-selected TCP destination, or guest-loopback shortcut.
The current tree has no production container or microVM launcher. In particular, the Firecracker/libkrun code is research-only and provides neither vsock nor a shared socket registration seam; doctor reports microVM guest transport as unavailable and the Firecracker fixture exits unavailable. The same-host netproxy primitive is likewise not exposed through runtime MCP grants; no macOS VM/container guest transport is claimed. See ADR 0141-runtime-grants.
Tier 3 — Kernel Module (strongest isolation)
A kernel module (eBPF LSM on Linux, macOS SystemExtension) intercepts all file, network, and process events system-wide, regardless of whether the agent runs in a container or directly on the host.
What it adds over Tier 2:
- Covers any process on the machine, not only agents that agentjail spawned.
- No agent cooperation required: works even if the agent binary is replaced or modified.
- Suitable for fleet-wide deployment where every machine needs a consistent enforcement boundary.
Setup
# One-time setup
agentjail install --for claude-code
# → writes hook entry to ~/.claude/settings.json
# → starts agentjail-daemon as a launchd service
# → writes default ~/.agentjail/policy.yaml
# Then use your agent normally
claude # every tool call is now policy-checked
Related Docs
docs/FLOW.md-- plain-language walkthrough of a shielded session and the allowed-hosts modeldocs/PRINCIPLES.md— design constraints and trade-offsagentpolicy/docs/DECISION_RPC.md— daemon RPC protocoldocs/adr/— architectural decision records