The one thing to internalize: a .warrant sidecar and a claims.json are
executable input whenever they carry C4 (pytest:) or C5 (shell:) checkers.
This doc states exactly what dorian does and does not protect, in the project's
FACT / LIMITATION / SAFE DEFAULT / UNSAFE / EXAMPLE format.
| Checker | Form | Executes code? | What it does |
|---|---|---|---|
| C1 | span anchor | No | reads a sealed read-set entry in-process |
| C3 | path / symbol / string / regex |
No | reads files in-process (regex match runs in a killed-on-timeout worker) |
| C5 typed | rowcount / schema / nullrate / domain / freshness / snapshot / reconcile |
No | reads CSV/SQLite/parquet read-only |
| C4 | pytest:<nodeid> |
Yes | spawns python -m pytest |
| C5 shell | shell:<command> |
Yes | spawns an arbitrary command |
The classification lives in one function, dorian.policy.executable_kind, and
the deny-exec gate and these docs both derive from it.
- Runs the checker programs found in the sidecar / claims file on the machine that invokes it, with the caller's privileges.
- Strips the environment of executed checkers to a small allowlist
(
PATH,HOME,LANG,LC_ALL) so secrets in other env vars do not leak in. - Confines checker file references to the repo root (path-escape attempts ERROR).
- Resolves a C4 test's import dependencies statically at seal/rebind time
(stdlib
astover tracked.pyfiles only): it parses source to widen the re-check watch set — it never imports application modules, executes setup code, mutatessys.path, inspects installed packages, or reaches the network, and an unresolvable/untracked/ambiguous import simply adds nothing (src/dorian/test_deps.py). - Bounds C3
regex:patterns to 500 chars, compile-guards them, and runs the match in a worker process killed attimeout_sso catastrophic backtracking cannot stall the run (ERRORregex_timeout). - Provides
--deny-exec/--deny-shell(andDORIAN_DENY_EXEC/DORIAN_DENY_SHELL) to refuse the executable families entirely.
- Not a sandbox. An allowed C4/C5 checker runs real code with your privileges; dorian does not jail the filesystem, network, or process.
- Scope-lint (
[tool.dorian.scopes]) restricts which files a claim may name; it does not restrict what an executed checker may read or write. - The env strip reduces, but does not eliminate, exposure:
PATH-resolved tools and on-disk secrets are still reachable by an allowed checker. - deny-exec stops the executable families from running; it does not make a with-exec run safe.
Use dorian where you already trust everyone who can write a .warrant or a
claims.json: your own repo, an internal repo, a team you trust. Review claims
before sealing exactly as you review code. This is the supported posture.
# trusted local loop
dorian verify note.md --claims claims.json
dorian revalidate --since HEADDo not run verify / seal / revalidate / rebind on claims from a
source you do not trust without --deny-exec (rebind re-runs every checker to
re-seal, so it executes code too). Do not use pull_request_target with an
untrusted-head checkout.
# untrusted context: remove the ability to execute code
dorian verify note.md --claims claims.json --deny-exec
DORIAN_DENY_EXEC=1 dorian revalidate --since origin/mainA blocked checker ERRORs, so a blocked load-bearing claim cannot seal and cannot silently pass revalidation — deny-exec fails closed.
dorian revalidate --checker-source base (Action input checker_trust: base)
resolves each claim's checker SPEC from the trusted base ref, then runs it
against the PR-head sources. A PR-added or PR-modified executable (C4/C5 shell:)
checker is therefore never executed; a rewritten checker cannot self-attest a
verdict (the base spec wins, and the change is surfaced); a missing or tampered
base sidecar fails closed (ERRORED, never executed). The
trusted-base test matrix proves each case with a
filesystem side effect that must not appear.
This is a checker-source trust root, not a sandbox. A base-approved pytest:
checker can still import and execute PR-head code, so the honest recommendation for
public forks is checker_trust: base with deny_exec: true (or stronger
external isolation), never "safe for arbitrary fork PRs". The four conditions below
now hold for the trust-root threat; sandboxing executed code remains out of scope.
- ✅ Checker specs are taken from the trusted base ref (
--checker-source base). - ✅ deny-exec is available and recommended for fork PRs (
deny_exec: true). - ✅ Tests simulate a fork/head sidecar trying to execute shell/pytest and prove it
is not run (
tests/test_trusted_base.py). - ✅ No
pull_request_targetin the documented workflow.
The residual, stated plainly: even in base mode a base-approved code-executing
checker runs PR-head code, so without deny-exec or external sandboxing this is
not safe for fully untrusted code. For trusted/internal repos, head mode
remains correct and unchanged.
A second residual — claim selection and read-set still come from the PR-head
sidecar, not base. --checker-source base substitutes only the checker spec.
The set of claims that get re-checked (each claim's watch) and the read-set
hashes a non-executing C1/snapshot: checker compares against are still read
from the PR-head .warrant. So a hostile fork that rewrites its own committed
sidecar can (a) drop a file from a claim's watch to suppress that claim's
re-check, or (b) forge a read-set hash so a C1/snapshot: base-spec checker
passes against altered content — the content-address check only proves the warrant
is internally consistent, which a fork can recompute. Base mode therefore hardens
the executable-checker path (a PR's malicious pytest:/shell: spec never
runs), but is not a complete defense against a hostile fork's sidecar metadata.
Deriving selection and the read-set from the base ref as well is tracked
hardening (see NEXT_ALGORITHMIC_BETS.md); until
then, treat checker_trust: base as a checker-spec trust root for semi-trusted
contributors, and for genuinely untrusted forks rely on required review of the
.warrant diff and branch protection (plus deny_exec), not on base mode alone.
The v1.4 governance adapter adds a host-side enforcement layer. The boundary is deliberate:
dorian gateand the core never veto.gateemits acontinue/repair/escalatedecision and exits only0/4(or2for malformed input). It never uses exit 2 as a veto, never reads a clock, and never reads a nonce — the verdict path stays pure and portable.- The exit-2 tool-block lives in the Claude Code
PreToolUsehost hook. Under a strict policy (DORIAN_POLICY=unattendedorDORIAN_EFFORT=godmode) the veto fails closed: a missing, stale (older thanFRESH_SECONDS = 900), or identity-mismatched decision packet (repo_root/base_ref/nonce) blocks the mutating tool, as does a standingescalate. Under an attended policy it fails open — a human is present, so the agent is not trapped — except a standingescalateon a sensitive path still blocks. - Still not a sandbox. The veto can refuse to start a mutating tool; it does not contain what
an allowed tool then does.
.dorian/local/last-decision.jsonis ephemeral, gitignored runtime state — never an authoritative ledger, and never read back into a verdict.
The verdict path is model-free, but the translation of human intent into checker specs and a goal
record happens at emission — drafted by an agent or a human, before any check runs. That
translation can drift from intent: a checker can be well-formed yet too weak to falsify the claim's
kind (a behavior claim backed only by a symbol: existence check). --strength-gate and the
binding diagnostics catch categories of weak backing, but a checker that is adequate-by-grammar
yet semantically off-target can still pass. Treat claim/goal emission as a reviewed step —
VALIDATION_HONESTY.md is the contract for what a passing checker does
and does not prove.