Architecture › System atlas
System atlas
Doberman sits between a coding agent and its tools and turns every meaningful action into a risk-based PASS, AUTH, or BLOCK decision. This page maps the whole system for anyone deciding whether to trust it or contribute to it.
On this page
One path in, no path around it#
Two front doors reach Doberman: host hooks running inside an agent harness (Claude Code, Codex, OpenClaw), and an MCP proxy that wraps any tool server for any MCP client. Both normalize the tool call into the same SecurityObject and hand it to one decision engine. There is no third path.
flowchart LR
subgraph AGENTS["Protected agents"]
CC["Claude Code"]
CX["Codex CLI"]
OC["OpenClaw"]
MC["Any MCP client"]
end
subgraph FRONT["Front doors (mediation)"]
HH["hosthooks<br/>spine.py"]
PX["proxy<br/>serve.py / executor.py"]
end
subgraph CORE["Policy core (never imports proxy)"]
SO["SecurityObject<br/>(models.py)"]
ENG["engine<br/>decision_engine.py"]
OBJ["objective rules"]
SUBJ["subjective layer"]
POL["policy<br/>modes / checklist / drift"]
end
AUTH2["auth<br/>challenge + prompters"]
ST["storage<br/>redacted log + sinks"]
SURF["surfaces<br/>cli / dash / tui"]
TOOL[("real tool")]
CC -- "PreToolUse hook" --> HH
CX -- "hook shim" --> HH
OC -- "hook" --> HH
MC -- "MCP calls" --> PX
HH -- "normalize" --> SO
PX -- "normalize" --> SO
SO --> ENG
OBJ --> ENG
SUBJ -- "raise-only" --> ENG
POL -- "mode + checklist" --> ENG
ENG -- "AUTH" --> AUTH2
AUTH2 -- "approved / denied" --> ENG
ENG -- "PASS" --> TOOL
ENG -- "record (redacted)" --> ST
ST --> SURF
classDef amber fill:#b45309,color:#fff,stroke:#b45309
classDef pass fill:#3e7d5a,color:#fff,stroke:#3e7d5a
class AUTH2 amber
class TOOL pass
Both front doors converge on one SecurityObject, then one engine; a BLOCK means the downstream tool never sees the call.
See the full lifecycle of one tool call in the decision path, and the verdict order in verdicts.
The policy core never imports a front door#
Inside the core package, the safety-critical modules (the decision engine, roles, policy, storage, auth, the subjective layer, and the egress broker) never import the proxy, the host hooks, the dashboard, or the turn gate. Adapters depend on the engine; the engine never depends on an adapter. An import-linter contract enforces this in CI, so a pull request that adds a stray import from the engine into the proxy fails the build before anyone has to catch it by eye.
That is why a rule, an auth flow, or a redaction fix can be read and tested without knowing which harness triggered it, and why no host-specific shortcut can quietly change what a decision means.
The front doors, in detail#
Host hooks (doberman/hosthooks/) share one spine (spine.py) across every adapter: claude_code.py, codex.py, openclaw.py. The spine checks the project exclusion list first, so an uninstalled project's hooks skip cleanly, then runs evaluate_pre / evaluate_post against the engine. hookio.py owns the shape of a deny response and resolves AUTH; a broad exception anywhere in that path denies rather than passing the call through. singleflight.py collapses duplicate hook invocations so one tool call isn't evaluated twice.
The MCP proxy (doberman/proxy/) does the same job for any MCP client: serve.py runs the stdio server, normalize.py turns the MCP call into a SecurityObject, and executor.py (decide_and_execute, _handle_auth) drives the same engine call the host hooks use.
Both front doors also protect their own configuration. An agent cannot edit .doberman/, a host's settings files, or the hook exclusion list to route around the guard; those paths are confined the same way any other protected path is.
flowchart TB
subgraph HOSTS["hosthooks"]
CCA["claude_code.py"]
CXA["codex.py<br/>(CC-compat shim)"]
OCA["openclaw.py"]
SP["spine.py<br/>1. is_excluded? (uninstalled projects)<br/>2. evaluate_pre / evaluate_post"]
IO["hookio.py<br/>deny shape · resolve_auth<br/>broad-except → deny"]
SF["singleflight.py<br/>duplicate-hook collapse"]
CCA & CXA & OCA --> SP --> IO
SP --- SF
end
subgraph PROXY["proxy (MCP)"]
SV["serve.py<br/>MCP stdio server"]
EX["executor.py<br/>decide_and_execute · _handle_auth"]
NM["normalize.py"]
SV --> NM --> EX
end
SP -- "SecurityObject" --> ENG2["engine.decide"]
EX -- "SecurityObject" --> ENG2
ENG2 -- "BLOCK" --> NOTH["downstream server<br/>records nothing"]
INST["install.py / install_codex.py<br/>hook registration + gated uninstall"] --- SP
classDef block fill:#a83a32,color:#fff,stroke:#a83a32
class NOTH block
One spine for every host adapter, one executor for the MCP proxy, both handing a SecurityObject to the same engine.
Wiring a new host adapter is covered in the host adapter guide; day-one setup for either front door is in setup.
Surfaces: where a human looks#
The CLI (doberman/cli/, built on Typer) is the control room: mode and setup change the strength mode, tune reads friction telemetry, doctor runs health checks, and log / memory / policy-history read the decision history. The dashboard (doberman/dash/, a local Starlette app) mirrors the live feed and mode control behind a bearer token printed to the terminal that launched it. Every mutating endpoint checks that token the same way, so a local agent cannot curl its own way to a looser policy. The TUI (tui.py) browses the redacted log with a plain-language "why" pane (explain.py): a deterministic, offline narrator whose wording can optionally be reworded by an LLM, but the wording can never change the verdict it explains.
flowchart LR
subgraph CLI3["cli/main.py (Typer)"]
MODE["mode / setup<br/>→ policy.apply_mode_change"]
TUNE["tune<br/>friction telemetry, --accept<br/>routes the weaken chokepoint"]
DOC["doctor.py<br/>health checks"]
LOGC["log / memory / policy-history<br/>(compact --json contract)"]
end
subgraph DASH["dash/app.py (Starlette)"]
TOK["bearer token (per-run,<br/>printed to launching terminal)"]
FEED["live feed + stats + pending"]
RESV["/api/resolve — approve/deny"]
MODE2["/api/mode — mode control<br/>same gate as CLI"]
end
TUI2["tui.py — log browser"]
EXPL["explain.py — offline 'why',<br/>LLM narrator opt-in, never the verdict"]
ST2["storage (redacted)"] --> LOGC & FEED & TUI2
EXPL --- TUI2
TOK --- RESV & MODE2 & FEED
MODE & MODE2 --> GATE2["policy drift gate"]
classDef amber fill:#b45309,color:#fff,stroke:#b45309
class GATE2 amber
The CLI's mode and the dashboard's /api/mode both call the one shared apply_mode_change path, so changing the strength mode from either surface goes through the same gate.
The full command reference is in the CLI reference.
Discovery#
doberman/discovery/ scans the MCP configurations already on the machine and reports what agents could reach: a read-only local risk map. It never modifies a config it reads.
Extension seams#
Core declares interfaces and a registry; anyone can ship a package that registers implementations through their own pyproject.toml entry points: a rule (doberman.rules), a detector (doberman.detectors), an audit sink (doberman.audit_sinks), an auth provider (doberman.auth_providers), a policy source (doberman.policy_sources), or a cost observer (doberman.cost_observers). Core never imports a plugin by name; it discovers whatever is installed at runtime. With nothing but doberman installed, the built-in rules and behavior run in full. Plugins add capability; they aren't required for the base guarantee. CI proves this: one job builds and tests core with no plugin package installed at all.
flowchart LR
subgraph CORE2["doberman (public, Apache-2.0)"]
R1["Rule seam<br/>entry point: doberman.rules"]
R2["Detector seam<br/>doberman.detectors"]
R3["AuditSink seam<br/>doberman.audit_sinks"]
R4["AuthProvider seam"]
R5["PolicySource seam"]
R6["CostObserver seam"]
REG2["registry — entry-point discovery"]
R1 & R2 & R3 & R4 & R5 & R6 --- REG2
end
subgraph PLUG["Any installed package"]
EX1["example plugin<br/>(examples/ tutorial)"]
end
EX1 -- "registers via entry points" --> REG2
CI2["CI guards:<br/>import-linter contracts<br/>standalone install+test job"] --- CORE2
Any installed package can register through the same entry-point groups core itself defines.
Read the plugin guide to write one. Each subsystem this page only summarizes (the objective guardrail, the subjective layer, the turn gate, policy and drift defense, storage and audit, and the auth flow) has its own concept page; the decision path is the place to start. Parity shows which guarantees are proven on which host.