# Doberman - Setup Guide
The complete guide to running Doberman in front of your coding agent: install it, wire it to
your agent, verify it, and watch it work. New here? The
[README Quick Start](../README.md#quick-start) has the 30-second version.
**Contents**
- [1. Install](#1-install)
- [2. The fast path: `doberman setup`](#2-the-fast-path-doberman-setup)
- [3. Wire it to your agent](#3-wire-it-to-your-agent)
- [Host hooks](#claude-code-hooks) - Claude Code and Codex CLI
- [MCP proxy](#mcp-proxy) - Claude Desktop, Cursor, any MCP client
- [OpenClaw](#openclaw)
- [4. Lock it in: password and 2FA](#4-lock-it-in-password-and-2fa)
- [5. Check it's healthy: `doberman doctor`](#5-check-its-healthy-doberman-doctor)
- [6. Watch it work](#6-watch-it-work) - session summary, log and TUI, dashboard, demo
- [Appendix: wrong or stale `doberman` on PATH](#path-troubleshooting)
---
### 1. Install
```bash
pip install doberman-core
```
> The distribution is **`doberman-core`** (the bare `doberman` name on PyPI belongs to an
> unrelated, abandoned project). The import name and CLI are unchanged - after install you
> still `import doberman` and run the `doberman` command.
Or install the latest from source:
```bash
pip install git+https://github.com/fu351/Doberman-Core.git
```
Or for development:
```bash
git clone https://github.com/fu351/Doberman-Core.git
cd Doberman-Core
pip install -e ".[dev]"
```
Either way you get the `doberman` CLI on your PATH. If `doberman` behaves oddly - an old
version, a missing command - see the [PATH appendix](#path-troubleshooting).
(Maintainers: see [`RELEASING.md`](../RELEASING.md).)
### 2. The fast path: `doberman setup`
On Claude Code, one command does the whole job. An interactive wizard picks your alertness
mode, tunes your guardrails, and wires the hooks:
```bash
doberman setup # interactive: choose mode, guardrails, install scope
doberman setup --yes # accept sensible defaults (balanced mode), non-interactively
```
Basic protection works immediately, out of the box. When the wizard finishes:
[set a possession factor](#4-lock-it-in-password-and-2fa), then verify with
[`doberman doctor`](#5-check-its-healthy-doberman-doctor).
On a different host, or want to see what gets wired? The next section covers each path by hand.
### 3. Wire it to your agent
Pick the row that matches your host:
| Your host | How Doberman attaches | Where |
|---|---|---|
| **Claude Code** | Hooks - gate every built-in *and* MCP tool call (recommended) | [`doberman setup`](#2-the-fast-path-doberman-setup) or [host hooks](#claude-code-hooks) |
| **Codex CLI** | Hooks | `doberman install-hooks --host codex` - [host hooks](#claude-code-hooks) |
| **Claude Desktop / Cursor / any MCP client** | MCP proxy - wrap your tool server | [MCP proxy](#mcp-proxy) |
| **OpenClaw** | Native plugin adapter | [OpenClaw](#openclaw) |
#### Host hooks (Claude Code and Codex CLI)
Hooks make Doberman gate **every** tool call your agent makes - built-ins (`Bash`, `Edit`,
`Write`, ...) *and* any MCP tool - without rewiring your MCP config. The harness calls Doberman
before each tool call, and Doberman answers **allow / deny**. A sensitive action opens
Doberman's own in-session approval dialog (confirm / TOTP 2FA), so the agent can't bypass it by
simply not "asking to use Doberman".
Install them with one command:
```bash
doberman install-hooks # Claude Code: writes .claude/settings.json (this project)
doberman install-hooks --global # ~/.claude/settings.json (every project)
doberman install-hooks --host codex # Codex CLI: wires `doberman hook codex-pre` instead
doberman install-hooks --dry-run # show what would change, write nothing
doberman uninstall-hooks # remove only Doberman's entries (leaves your other hooks intact)
```
`install-hooks` is idempotent (safe to re-run), backs up an existing `settings.json` before
writing, and never touches your other settings or hooks. `doberman setup` above runs it for you.
On Claude Code it writes this snippet - or add it by hand:
```jsonc
// .claude/settings.json (this project) or ~/.claude/settings.json (all projects)
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|Edit|Write|NotebookEdit|WebFetch|WebSearch|mcp__.*",
"hooks": [{ "type": "command", "command": "doberman hook pre" }]
}
],
"PostToolUse": [
{
"matcher": "Bash|Edit|Write|NotebookEdit|WebFetch|WebSearch|Read|Glob|Grep|mcp__.*",
"hooks": [{ "type": "command", "command": "doberman hook post" }]
}
],
"SessionStart": [
{
"hooks": [{ "type": "command", "command": "doberman session-summary" }]
}
]
}
}
```
**What the pre-hook does.** `doberman hook pre` reads the tool call on stdin and runs
Doberman's deterministic **objective floor**: path confinement, destructive commands,
external-destination and secret-exfil checks, smuggled-token channels. Then it decides:
- A routine action passes silently. Doberman is raise-only - it never strips the harness's
own prompts.
- A sensitive action **opens Doberman's approval dialog**: a topmost confirm / TOTP-2FA prompt
bound to that exact action. Approve, and that single call is allowed. Decline - or if no GUI
or terminal channel is available - and it's denied, fail-closed.
- A dangerous action is blocked, with a redaction-safe reason.
**What the post-hook does.** `doberman hook post` runs after a tool executes and scans the
tool's **output** for credential-like material:
- Output containing a **recognizable credential** (a known key shape, a PEM block, a secret
file's contents) is **blocked from reaching the model**. The secret is never echoed.
- A merely **high-entropy** token with no known credential shape (a hash, a UUID, a base64
fragment) passes through - that heuristic false-positives on ordinary output - but it is
still **recorded and taints the session**.
- Session taint powers a **multi-step exfiltration floor**: the pre-hook raises any later
egress (web, network, MCP) in a session that has already touched a secret - `ask` in
light/balanced, a hard `deny` in strict/paranoid. That catches read-secret-then-send-it
exfil no single-call rule can see.
- When an outbound value *exactly* matches (by keyed-HMAC fingerprint) a secret that entered
the session earlier, that **confirmed** read-then-send is a hard `deny` in **every** mode,
even `light`.
Both handlers **fail closed** and are import-light, so they add minimal latency to each call.
Every decision lands in the same local, redacted history: `doberman log` shows PreToolUse
AUTH/BLOCK outcomes alongside PostToolUse ones, and `doberman status` reports the installed
version, which settings file(s) have the hooks wired in, and the last 5 recorded decisions.
**Doberman protects its own hooks.** Once installed, the agent can't quietly remove them. A
write or edit to `.claude/settings.json` (the hook-install file) is **blocked**, and other
`.claude/` changes require authentication - the agent can't disable enforcement by editing the
harness config ("firing the cop"). This mirrors how Doberman already hard-blocks its own
`.doberman/` control plane. The protection holds **through the shell** too: a Bash command that
writes or deletes the config (`echo > .claude/settings.json`, `rm -rf .doberman`) or runs
`doberman uninstall-hooks` is blocked, not just the `Write`/`Edit` tools. The same shell-layer
block extends to every posture- and auth-mutating Doberman verb - `mode`, `prefs`,
`enforcement`, `2fa`, `password`, `revoke`, `taint` - treated as control-plane tampering and
blocked fail-closed, while read/utility verbs (`status`, `doctor`, `log`, `scan`, `review`)
stay allowed.
#### MCP proxy - wrap any tool server
Doberman is a transparent MCP proxy. You give it your existing tool server command after `--`,
and it intercepts everything in the middle:
```bash
# Before - agent talks directly to your tool server:
npx -y @modelcontextprotocol/server-filesystem ~/my-project
# After - wrap it with Doberman:
doberman serve -- npx -y @modelcontextprotocol/server-filesystem ~/my-project
# ^^ the -- separator: everything after is your existing tool server command
```
To specify which repo's policy governs decisions (defaults to the current directory):
```bash
doberman serve --path ~/my-project -- npx -y @modelcontextprotocol/server-filesystem ~/my-project
```
Doberman communicates over **stdio**: it spawns your tool server as a managed subprocess and
speaks standard MCP. Your agent sees one server entry; the real tool server runs silently
behind it.
> **You don't run `doberman serve` yourself, and it doesn't start your agent.** Your agent's
> MCP client spawns it, using the config below. Typed bare into a terminal it just blocks on
> stdin waiting for a client to speak MCP, which looks like a hang; it prints one line saying so.
Then point your agent at Doberman - replace your agent's existing MCP server entry with the
Doberman-wrapped version.
**Claude Code (CLI):**
```bash
claude mcp add doberman -- doberman serve -- npx -y @modelcontextprotocol/server-filesystem ~/my-project
```
**Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json` on Mac,
`%APPDATA%\Claude\claude_desktop_config.json` on Windows):
```json
{
"mcpServers": {
"doberman": {
"command": "doberman",
"args": ["serve", "--",
"npx", "-y", "@modelcontextprotocol/server-filesystem", "~/my-project"]
}
}
}
```
**Cursor, Codex, or any MCP-compatible client**: use the same `mcpServers` format in your
client's MCP config file, substituting your own tool server command after `--`.
Note the proxy protects the tools you route *through* it. To gate the agent's built-in tools
too (`Bash`, `Edit`, `Write`, ...), use [host hooks](#claude-code-hooks) where your host
supports them.
#### OpenClaw
[OpenClaw](https://docs.openclaw.ai) agents route through Doberman via a small local plugin
instead of a hook-pack (OpenClaw's `before_tool_call` event is only reachable from a typed
plugin hook). It spawns `doberman hook openclaw` per call - the same fail-closed, deterministic
objective floor as the Claude Code hook - and maps the verdict to OpenClaw's own primitives:
`allow` is a no-op, `block` is terminal, and `auth` delegates to OpenClaw's own `/approve` flow
(the gateway has no interactive terminal of its own for Doberman's local challenge dialog). See
[`adapters/openclaw/README.md`](../adapters/openclaw/README.md) for install steps and the
mandatory "verify it's live" canary check - OpenClaw has shipped bugs where plugin hooks
silently never fire, so that check isn't optional.
### 4. Lock it in: password and 2FA
Doberman is raise-only: tightening is always free, but any later *permanent* policy lowering
must prove possession of a local factor. Set the minimum factor now; TOTP enrollment is
optional, but becomes the required (stronger) factor when present:
```bash
doberman password set # always-available minimum for mode/prefs lowerings
doberman 2fa setup # optional TOTP; required instead of the password once enrolled
```
Rotating or dropping TOTP both need the code you currently hold, so a lost authenticator
can't be swapped out by anyone who merely reaches your shell:
```bash
doberman 2fa setup --force # rotate to a new secret (proves the current code first)
doberman 2fa remove # unenroll; weakenings fall back to the password afterwards
```
Removing the last possession factor is allowed but fails *closed*: with neither TOTP nor a
password enrolled, every policy weakening is denied until you enroll one again.
The same enrolled factor also gates the one other recovery action: a secret read taints a
session for the rest of it, and in strict/paranoid that raises later egress to AUTH or BLOCK
with no automatic reset. If that's expected and you want the repo's egress back to the mode
default, `doberman taint clear` wipes both taint stores after the same TOTP-or-password check.
It still fails closed with neither factor enrolled, and a denied or failed check leaves
everything untouched.
### 5. Check it's healthy: `doberman doctor`
One read-only self-check that answers *"is Doberman actually wired up and healthy?"* - host
hooks, config, the decision DB, 2FA, the enforcement dial + strictness mode, and the
fingerprint key:
```bash
doberman doctor # prints a green/red checklist; exits non-zero if a critical check fails
```
It only diagnoses (never changes state) and exits non-zero when a critical check - hooks,
config, or the decision DB - isn't healthy, so it's safe to gate a script on
`doberman doctor && ...`.
Optionally, map what Doberman can see:
```bash
doberman scan # discover local MCP capabilities and build a risk map
```
### 6. Watch it work
#### Session summary
`install-hooks` also wires a `SessionStart` hook that runs `doberman session-summary`: a
print-and-exit (never interactive, never blocking) summary of a **device-global, lifetime
rollup**. Every decision Doberman makes, across every repo and session on this machine,
increments a tiny counter at `~/.doberman/metrics.db` - verdict class + count only, no path,
no reason code, no per-action detail. The former `doberman dashboard` command remains as a
hidden compatibility alias. It shows total interceptions and the PASS/AUTH/BLOCK split:
```
+------------------------------------------+
| Doberman - session guard summary |
| Tracking since 2026-06-14 - this device |
| |
| Interceptions 1,204 |
| Auto-passed 1,131 ( 93.9%) |
| Authed 58 ( 4.8%) |
| Blocked 15 ( 1.2%) |
+------------------------------------------+
```
Run it any time with `doberman session-summary`. Output is plain ASCII (no box-drawing runes
or emoji) so it always renders on a legacy Windows console, and the command always exits `0`
and never raises - a session summary must never break a session start.
#### Decision log and TUI
`doberman log` prints the raw redacted rows; `doberman tui` browses the same rows
interactively and adds a plain-language "why" for whichever row is highlighted - the verdict,
the decided layer, and its reason codes turned into a sentence, using only that row's
already-redacted data (never a raw path, argument, or secret). Arrow keys navigate, `r`
reloads, `q` quits:
```bash
pip install "doberman-core[tui]" # optional extra (textual)
doberman tui
```
By default the "why" is a deterministic, offline template - no network call, always
available. You can optionally enrich it with a short Claude-Haiku rewrite in plainer language:
```bash
pip install "doberman-core[explain]" # optional extra (anthropic)
export ANTHROPIC_API_KEY=...
export DOBERMAN_EXPLAIN_LLM=1 # opt-in; off by default
doberman tui
```
The LLM is a **narrator, never a judge**: it only rewords a verdict Doberman already made
from the redacted metadata above; it can never change a decision. It's strictly opt-in
(installed *and* keyed *and* flagged, all three), and any failure - missing key, no network,
timeout, bad response - silently falls back to the offline template, so the TUI never blocks
on it or crashes because of it. There is no `doberman explain` command; the TUI and
`doberman log` are the only surfaces for this.
#### Dashboard (preview)
```bash
pip install "doberman-core[dash]" # optional extra: starlette + uvicorn
doberman dash --path . # prints a URL, e.g. http://127.0.0.1:8642/?token=...
```
A localhost-only web dashboard, off by default. Binds to `127.0.0.1` only (never a public
interface) and generates a fresh, single-use token for that run - open the printed URL to
connect; every API call is authenticated with that token. `--path` selects the repo to report
on (default: the current directory).
Now live: a **summary stats line** (verdict counts, top reason codes, secret/taint event count,
current mode + effective enforcement - `GET /api/stats`) and a **scrolling live decision feed**
(`GET /api/feed`, Server-Sent Events) that backfills the most recent decisions on connect, then
streams new ones as they're recorded. Both are read-only and serve only already-redacted decision-
log fields (verdict, action type, path *class*, reason codes, timestamp) - never a raw target,
argument, or secret. `EventSource` can't set request headers, so the feed also accepts the token
as `?token=` (loopback-only + single-run token keeps this sound).
**Interactive AUTH approve/deny.** An `AUTH` challenge can now be answered from the dashboard
instead of the terminal: `GET /api/pending` lists redacted pending approvals (action type, risk,
reason codes, human explanation, path *class* - never a raw target or secret) and
`POST /api/resolve/{id}` (body `{"decision": "approved"|"denied", "totp_code"?}`) answers one.
Resolution is a single-use, race-safe state transition (`UPDATE ... WHERE status='pending'`) -
two concurrent resolves of the same row can never both win, and a resolved/expired row 409s.
The dashboard never verifies a TOTP code itself: it only relays the human's decision (and, for
tiers that need one, the code) back to the *existing* auth-challenge machinery running in the
decision path, which performs the real verification unchanged. The channel engages only while a
dashboard's liveness heartbeat is fresh (< 5s old); a stale or missing heartbeat, or an
unanswered approval, falls back to the next channel (MCP elicitation -> GUI dialog -> terminal)
with no added latency and no denial invented on the dashboard's behalf.
**Visual polish.** Dark-by-default (a `prefers-color-scheme: light` override is available),
with color-coded PASS/AUTH/BLOCK and risk badges in the live feed and pending-approval cards, a
header bar showing the current mode + effective enforcement at a glance, and a designed empty
state before any decisions arrive - no build step, no external assets, works fully offline like
the rest of the shell.
#### Try the demo
Want to see real verdicts light up the dashboard without wiring up an agent? `doberman demo`
runs a scripted "attack reel" - five malicious tool calls and two benign ones - through the
**real** decision engine (no stubs) and logs every verdict, so the dashboard's live feed lights
up with genuine PASS/AUTH/BLOCK decisions. Nothing is ever executed against a real tool or
downstream server.
```bash
# Terminal 1
doberman dash --path .
# Terminal 2
doberman demo --path . # add --fast to skip the pacing delay between scenarios
```
Each scenario prints one line (verdict, reason codes, explanation - never the raw tool
arguments or any synthetic secret used to trip a rule), then a summary table. Exit code is `0`
only if every scenario matched its expected verdict, so `doberman demo` doubles as a smoke test
of the engine itself.
### Appendix: wrong or stale `doberman` on PATH
If `doberman` behaves unexpectedly - missing a command you just added, using an
old version, or ignoring changes from your dev install - the shell may be
resolving a *different* `doberman` executable than the one in your active
virtual environment. This is common when you have more than one installation
method in play (global `pip`, `pipx`, and one or more venvs).
This section only lists and compares what's already on your PATH. It does not
modify PATH, uninstall anything, or touch your environments.
**List every `doberman` executable currently resolvable.**
Run the command for your shell. Each one lists **all** matches, not just the
first - this matters because the *first* result is the one actually being run.
```powershell
# PowerShell
Get-Command -All doberman
```
```cmd
:: Command Prompt (cmd.exe)
where.exe doberman
```
```bash
# Unix-like shells (bash/zsh/etc.)
which -a doberman
# or, more portable:
command -v doberman
```
If more than one path is listed, the first one in the output is the one your
shell will actually invoke when you type `doberman`.
**Compare the resolved executable against your active virtual environment.**
With your intended venv activated, check where Python thinks it's installed
and compare it to what step 1 found.
```bash
# Unix-like shells
python -c "import sys; print(sys.prefix)"
command -v doberman
```
```powershell
# PowerShell / Command Prompt
python -c "import sys; print(sys.prefix)"
Get-Command doberman
```
If `sys.prefix` doesn't match the directory the resolved `doberman` lives in
(e.g. it's not under `.venv/bin` or `.venv/Scripts`), a different install is
shadowing your venv's copy.
**Inspect common install locations safely.**
These only report information - they don't remove or modify anything.
```bash
# Check a pip-installed copy inside a venv (Unix-like shells)
.venv/bin/pip show doberman-core
```
```powershell
# Same, on Windows
.venv\Scripts\pip show doberman-core
```
```bash
# Check a pipx-installed copy (any shell with pipx on PATH)
pipx list
```
`pipx list` shows every pipx-managed package and the interpreter it's pinned
to, including any global `doberman` install that could be shadowing your venv.
```bash
# See which python/pip your shell defaults to (Unix-like shells)
which -a python python3 pip pip3
```
```powershell
# PowerShell
Get-Command -All python, pip
```
```cmd
:: Command Prompt
where.exe python
where.exe pip
```
**Remediation (non-destructive).**
Pick whichever fits your workflow - none of these require editing PATH or
removing anything:
- **Re-activate the intended virtual environment** in the current shell
session, then re-run step 1 to confirm it now resolves first:
```bash
source .venv/bin/activate # Unix-like shells
.venv\Scripts\activate # Windows (cmd or PowerShell)
```
- **Invoke the venv's executable explicitly**, bypassing PATH resolution
entirely:
```bash
./.venv/bin/doberman --version # Unix-like shells
.venv\Scripts\doberman.exe --version # Windows
```
- **Open a new shell/terminal window** if you recently activated or
deactivated an environment - some shells cache the resolved path for the
current session (`hash -r` in bash clears this without restarting).