Reason codes

September 17, 2026 · View on GitHub

Every non-PASS Doberman decision carries one or more ReasonCode values plus a human explanation. ReasonCode itself is defined in src/doberman/models.py. This page catalogues every member of that enum: where in the code it actually gets attached to a decision, and what it means in plain language. The enum currently defines 65 codes. doberman log and doberman tune --json (see CLI reference) both surface these values directly, so matching on the name here is the stable way to script against a decision.

CodeGroupRaised inMeaning
normalization_failedGeneralengine/rules/normalization.py, proxy/normalize.pyThe proxy's normalize() step raised, so the action falls back to a conservative high-risk placeholder and escalates to authentication instead of passing through un-normalized.
unknown_toolGeneral(reserved, enum only)Reserved for a requested tool name absent from the downstream tool list. Defined for that purpose but not wired to a raise site in shipped code.
downstream_errorGeneralproxy/executor.pyThe downstream tool call, made over MCP (Model Context Protocol, the interface Doberman's proxy uses to reach real tools), raised while being forwarded, so the result is denied and the failure is never exposed to the agent.
single_use_elevation_unclaimableGeneralproxy/executor.pyA single-use elevation covered the action, but the atomic claim taken just before the forward found it already spent (a concurrent call won it) or could not be recorded, so the forward is refused rather than let two calls release on one approval.
effect_set_divergedGeneralproxy/executor.pyThe proxy recomputes a delete-class command's bounded blast-radius effect set right before forwarding it. If that recompute's digest disagrees with what was shown to the human at approval time, in either direction, or a previously-known count becomes unknown, the forward is refused with a synthetic BLOCK rather than executed against a stale preview.
ambient_scoring_errorGeneralmonitor/daemon.pyThe ambient monitor (FM.2) could not reconstruct a SecurityObject from a redacted ActivityEvent, or scoring it through decide() raised. Per-event isolation records a conservative alert row instead of dropping the event silently or letting one poisoned event kill the daemon's tick loop. Never produced on the live/inline decision path.
objective_guardrail_errorGeneralengine/decision_engine.py, proxy/executor.pyThe objective guardrail, or a post-decision integrity check such as the output secret scan or artifact digest check, raised an unhandled exception, so the action fails closed to BLOCK.
subjective_guardrail_errorGeneralengine/decision_engine.py, engine/subjective.pyThe subjective guardrail raised an unhandled exception after the objective guardrail already passed, so the action escalates to authentication instead of silently passing.
subjective_block_clampedGeneralengine/decision_engine.py, policy/drift.pyThe subjective guardrail returned BLOCK, but none of its reason codes sit on the hard-block allowlist, so the execution rule clamps that BLOCK down to AUTH and records that the clamp happened.
proxy_handler_errorGeneralproxy/executor.pyAn unhandled exception escaped the proxy's top-level tools/call handler, outside normalize()'s and decide()'s own error handling, so the call is denied carrying only the exception's class name, never its message.
secret_exfiltrationObjective guardrailengine/rules/secrets.py, hosthooks/hookio.py, policy/modes.py, proxy/executor.pyA known credential or secret pattern, or a base64/hex payload that decodes to one, appeared in an action headed to an external destination. The rule hard-blocks it in every mode.
sensitive_secret_accessObjective guardrailauth/challenge.py, engine/rules/secrets.py, proxy/executor.pyThe action reads or targets a file recognized as holding secrets, such as .env or a private key, with no evidence it is leaving the session, so it requires authentication.
possible_high_entropy_secretObjective guardrailengine/rules/secrets.pyOnly the weak, purely statistical high-entropy heuristic matched: no known credential shape and no secret file, so the action requires authentication. It is a lower-confidence sibling of sensitive_secret_access, kept separate so output scanning can pass through likely-benign hashes, UUIDs, and base64 fragments instead of hard-blocking a benign read.
protected_path_blockedObjective guardrailengine/rules/commands.py, engine/rules/paths.py, policy/modes.pyThe target path resolves inside a location explicitly protected by policy, such as Doberman's own control-plane files, so the rule hard-blocks the action.
sensitive_path_accessObjective guardrailauth/challenge.py, engine/rules/paths.py, policy/drift.pyThe target path matches a sensitive-but-not-protected location, so the action requires authentication before it proceeds.
destructive_commandObjective guardrailengine/rules/commands.py, policy/modes.pyA shell or git command matched a pattern that irreversibly destroys data or history: a recursive force-delete, a history rewrite, or piping a downloaded payload into a shell. Also covers process kill/signal commands (kill/pkill/killall/taskkill, os.kill, psutil kill/terminate) at the AUTH tier; own-job targets (%N, $!) and probes (-0, -l) stay PASS.
environment_dump_commandObjective guardrailengine/rules/commands.pyA shell command whose sole effect is to enumerate or print the process environment (bare env, any form of printenv, export/export -p, declare -x/typeset -x with no named variable, or a PowerShell Env: drive listing). This is a locally scoped read of a common secret carrier, so it requires authentication before it runs.
bulk_operationObjective guardrailauth/challenge.py, engine/rules/commands.py, policy/drift.pyA command deletes at or above the configured bulk-operand threshold, so it requires authentication before the mass operation proceeds.
opaque_commandObjective guardrailauth/challenge.py, engine/rules/commands.pyA command's effect cannot be determined statically: an inline shell payload (-c ...), or a command that fails to parse safely. It requires authentication before it runs. This also covers runuser -c/--command/--command=, which is opaque exactly like su -c and never a transparently-consumed wrapper option. It also covers an inline Python/Node payload (python -c/node -e) that opens a socket directly with no shell-spawn, a raw-egress shape folded into this general opaque-inline-payload code rather than raw_socket_channel. (A payload that opens a socket AND spawns a subprocess/shell is a reverse shell, which goes to raw_socket_channel BLOCK instead, not this code.) It also covers an interpreter one-liner that spawns a subprocess (subprocess, os.system, child_process, …); the command literals it passes are walked by the same rule and can raise the step-up to BLOCK. Finally, it covers an inline payload fragmented into more than 128 candidates (a length proxy: ordinary one-liners measure 7–43) or naming more than 32 absolute/~/.. paths (the filesystem-resolve budget; every path is still matched textually). This last check is a floor applied after every stronger check, so a control-plane/destructive/socket/kill/privilege match on the same payload still BLOCKs/AUTHs at its own tier first.
unknown_external_destinationObjective guardrailauth/challenge.py, egress/local.py, engine/rules/destinations.pyA network destination is not on the trusted-host allowlist and cannot otherwise be classified as safe, so it requires authentication, subject to mode thresholds and any proven-broker override.
egress_requires_authObjective guardrailengine/rules/destinations.pyA shell, package-install, or git command has an external destination whose runtime route static parsing cannot prove, so it requires authentication regardless of how trusted the parsed host looks.
encoded_exfiltrationObjective guardrailauth/challenge.pyReserved for a base64/hex-encoded payload that decodes to a secret pattern. It is wired into auth tiering and policy, but the shipped secrets rule currently reports such decoded hits as secret_exfiltration or possible_high_entropy_secret, so this code is not presently emitted.
tool_schema_changedObjective guardrailproxy/executor.pyThe tool's (name, description, inputSchema) fingerprint no longer matches the pinned one, a possible rug-pull. This is AUTH in Light/Balanced and BLOCK in Strict/Paranoid until a human re-approves the pin.
rule_errorObjective guardrailengine/objective.py, engine/subjective.pyAn individual objective or subjective rule raised, or returned something other than a GuardrailResult, while being evaluated. That one rule's result is isolated to AUTH instead of forcing a silent PASS or an unrelated hard BLOCK.
role_blocked_targetRole policyengine/rules/role_boundary.py, policy/modes.pyThe active agent role's boundary matcher explicitly denies this target, so the rule hard-blocks the action.
role_out_of_scopeRole policyauth/challenge.py, engine/rules/role_boundary.py, policy/drift.pyThe action falls outside the boundaries declared for the agent's active role without being on an explicit block list, so it requires authentication (a role elevation).
policy_source_blockedRole policyengine/rules/policy_source.py, policy/modes.pyThe instruction's originating source is on a higher-authority policy source's block list, so the rule hard-blocks the action.
policy_source_sensitiveRole policyauth/challenge.py, engine/rules/policy_source.pyThe instruction's originating source is classified as sensitive, but not blocked, by the policy source, so it requires authentication.
unusual_for_workflowSubjective layerengine/subjective.py, policy/drift.pyThe action deviates from this agent's own learned per-workflow behavioral baseline (a running profile of what normal, everyday actions look like for that workflow), raising the subjective verdict.
unusual_for_deploymentSubjective layerengine/subjective.py, policy/drift.pyThe action's surprise score, computed across the universal three-axis algebra, exceeds this deployment's care threshold, raising the subjective verdict.
confidentiality_sensitive_destinationSubjective layerengine/subjective.pyAn above-threshold action's dominant care dimension is confidentiality, and the destination itself is sensitive.
irreversible_high_blastSubjective layerengine/subjective.pyAn above-threshold action's dominant care dimension is irreversibility and blast radius rather than confidentiality: it would be hard to undo and would affect a lot if it went wrong.
lethal_trifectaSubjective layerengine/decision_engine.py, engine/subjective.py, subjective/revealed.pyThe action's algebra shows untrusted-data provenance, access to private data, and an external communication channel all at once, flooring the verdict to AUTH (BLOCK in Strict/Paranoid). It is the only subjective code allowed onto the hard-block allowlist.
unclassified_actionSubjective layerengine/subjective.py, policy/drift.pyThe action's algebra classification is still effectively unknown by the time the subjective layer runs, so it is flagged rather than treated as benign.
pii_data_class_egressObjective guardrailengine/rules/data_classes.pyThe outbound payload contains checksum-valid personal or financial data: a payment card number (issuer prefix plus Luhn check), an IBAN (mod-97), or a dashed US SSN. Because the action has an external destination, every mode requires authentication. Only the class label is recorded, never the matched value.
smuggled_token_channelToken-channel defenseengine/rules/token_channels.pyA deterministic, near-zero-false-positive objective token-channel signature matched: tokens smuggled through an unexpected channel. It produces AUTH or BLOCK depending on severity.
anomalous_token_patternToken-channel defenseengine/detectors/token_channels.pyThe subjective token-channel detector found a soft, probabilistic out-of-distribution signal: homoglyph confusables (characters from a different alphabet that look identical to the expected one, like a Cyrillic "а" standing in for a Latin "a"), under-trained glitch fragments, NFKC-normalization smuggling, or anomalous control/private-use characters. It requires authentication.
oversized_encoded_blobEncoded-exfiltration detectorengine/detectors/base64_blob.pyThe subjective base64-blob detector found a suspiciously large base64-looking payload in a tool argument, a common encode-and-exfiltrate shape (file or secret contents smuggled out as one blob). It checks shape and size only, never decodes, and raises only to AUTH.
multi_step_exfilHost-hook containmentengine/taint_floor.py, hosthooks/hookio.pyThis egress action's session already carries taint, a marker meaning a secret was read earlier in the session, so anything sent out afterward is treated as suspect. Even though this call's own payload looks clean, the verdict is raised (AUTH, or BLOCK in Strict/Paranoid) as a potential cross-call exfiltration.
confirmed_exfilHost-hook containmentengine/taint_floor.py, hosthooks/hookio.pyAn outbound value's keyed-HMAC fingerprint (a one-way hash computed with a locally held key, so the value's identity can be matched without storing or exposing the value itself) matches a secret read earlier in this same session, confirming a read-then-send exfiltration. The rule hard-blocks it in every mode.
untrusted_value_echoHost-hook containmentengine/taint_floor.py, hosthooks/spine.py, proxy/executor.pyA host, URL, or email address in this call's outbound payload matches, by keyed-HMAC fingerprint, a value that first entered this session's context from an untrusted read (a WebFetch/WebSearch result). Whole-value match only, not flow analysis. Raises to AUTH, capped there in every mode (v1). Mail addresses are matched after undoing common [at]/(dot)/spaced obfuscation, so user [at] host [dot] com fingerprints the same as user@host.com. This is mechanical, not semantic, so a preposition next to a stray spaced period can occasionally be read as an address that was never actually named. That only ever produces an extra AUTH prompt on a later send to that exact address, never a silent allow.
turn_gate_errorTurn gateengine/decision_engine.py, turngate/heuristics.py, turngate/hook.pyThe turn gate, either its Tier 0 signature scan or its Tier 1 heuristics, raised while evaluating a turn. It escalates to authentication (fails upward) instead of silently passing.
instruction_nullificationTurn gateturngate/repeat.py, turngate/signatures.pyA turn's text matched a Tier 0 pattern telling the agent to ignore, disregard, or override its previous instructions. Doberman blocks it outright if issued or found in untrusted content, and requires authentication if merely discussed in typed text.
authority_overrideTurn gateturngate/repeat.py, turngate/signatures.pyA turn's text matched a Tier 0 authority-impersonation, jailbreak/mode-switch, or system-prompt-exfiltration pattern. Doberman blocks it outright if issued or found in untrusted content, and requires authentication if merely discussed.
secret_exportTurn gateturngate/repeat.py, turngate/signatures.pyA turn's text matched a Tier 0 credential, key, or token export request, for example "send me the API key." Doberman blocks it outright if issued or found in untrusted content, and requires authentication if merely discussed.
encoded_payloadTurn gateturngate/repeat.py, turngate/signatures.pyA turn contains a long, high-entropy base64/hex run or a punycode host, an encoded-carrier signature. Doberman blocks it in untrusted content and steps it up to authentication in typed content, since a user pasting base64 is plausibly legitimate.
indirect_injectionTurn gateturngate/repeat.py, turngate/signatures.pyA Tier 0 signature matched inside an untrusted, pasted or tool-fetched, content segment. Doberman treats this as indirect prompt injection and blocks it unconditionally, regardless of issue or mention framing.
embedded_instructionTurn gateturngate/heuristics.pyTier 1 heuristics detected an agent-directed imperative embedded in an untrusted content segment, escalating the turn to authentication.
persona_overrideTurn gateturngate/heuristics.pyTier 1 heuristics matched a persona- or role-override phrase in the turn, requiring authentication.
obfuscated_contentTurn gateturngate/heuristics.pyTier 1 heuristics found obfuscated or sub-threshold encoded content that does not reach the hard encoded_payload carrier threshold, requiring authentication.
urgency_secrecy_framingTurn gateturngate/heuristics.pyTier 1 heuristics matched language that combines urgency and secrecy framing, a common social-engineering pattern, requiring authentication.
stylometric_outlierTurn gateturngate/heuristics.pyThe turn's writing style is an extreme statistical outlier for this session and coincides with a sensitive apparent intent, requiring authentication. Style alone never triggers this on its own.
repeat_after_blockTurn gateturngate/hook.py, turngate/repeat.pyAn identical, same-fingerprint turn was resubmitted after being blocked. This converts the automatic block into a one-time AUTH challenge for the first repeat instead of blocking again outright.
turn_blocked_repeatedlyTurn gateturngate/repeat.pyA resubmitted turn's repeat challenge was already denied once, and the lockout threshold is reached. The rule hard-blocks further resubmission for a cooldown window instead of re-challenging.
egress_route_divergenceEgress brokerengine/rules/destinations.pyA broker-observed connection for this entity, within a bounded recent window, actually went somewhere that diverges from this rule's static host-trust classification. This retroactively raises the already-computed verdict.
egress_broker_enforcedEgress brokerengine/rules/destinations.pyA broker proven to enforce egress attests that this exact destination is allowlisted and will itself be enforced at the socket, so the rule contributes PASS instead of its usual AUTH.
egress_blocked_by_modeEgress brokerengine/rules/destinations.pyIn Paranoid mode only, a broker proven to enforce egress attests that this exact destination is not allowlisted and will itself drop it at the socket, so the rule hard-blocks instead of its usual AUTH. This code is dormant in every other mode, and with no broker present.
anomalous_egress_velocityEgress brokerengine/rules/destinations.pyA bounded, in-memory per-entity velocity tracker detected a burst, volume, or fan-out anomaly across this entity's broker-observed connections in the same recent window egress_route_divergence uses. This retroactively raises the already-computed verdict.
artifact_digest_mismatchArtifact digest verificationproxy/executor.pyA pinned expected sha256 digest exists for a previously fetched identity, and the freshly returned tool-result content's digest disagrees with it. The result is withheld from the agent post-fetch.
correlated_trifectaSession correlatorengine/correlator.pyThis session's recent decision history shows an untrusted-provenance ingress and a sensitive or secret read as separate earlier calls, and the current action is an external egress: a lethal trifecta assembled across calls rather than within one.
correlated_destructive_flowSession correlatorengine/correlator.pyThis session's recent decision history shows a broad read or enumeration and a shell command as separate earlier calls, and the current action is an external egress: a possible archive-then-exfiltrate flow.
raw_socket_channelObjective guardrailengine/rules/commands.pyA shell command opens a raw network channel outside the normal HTTP/tool egress path: a /dev/tcp//dev/udp redirection target, netcat/ncat/socat used in its exec-on-connect (reverse/bind-shell) form, or any openssl s_client invocation (a TLS client connection, whatever flag names the target). Detection reasons about command shape only and is deliberately narrow to keep the false-positive rate near zero. There are two tiers: the exec-on-connect subset (a socket wired to command execution, such as nc/ncat -e/--sh-exec, socat EXEC:/SYSTEM:, or an interpreter payload that opens a socket AND spawns a subprocess/shell) is an unambiguous reverse/bind shell and BLOCKs. The other shapes (/dev/tcp//dev/udp redirection, a port probe, openssl s_client, a bare inline socket) raise only to AUTH.
verification_bypass_flagVerification integrityengine/rules/commands.pyA git commit command carries --no-verify, --no-gpg-sign, or the short alias -n (including combined short flags like -an), skipping the commit's pre-commit/pre-push hooks or its cryptographic signature. This is a narrower, dedicated code, never destructive_command, so it can never sit on the mode-independent hard-block floor.
test_file_removalVerification integrityengine/rules/paths.pyThe target of a file_delete action, or of a file_write/file_delete action whose tool name contains "rename"/"move", matches a test-file glob (test_*.py, tests/**, *.test.js, *.test.tsx, *.spec.mjs, etc.). This is another shape of an agent quietly disabling its own safety net rather than fixing what it flags. The rule is scoped to the action type: an ordinary edit of a test file, or a non-mutating action (e.g. a read) whose tool merely happens to be named "rename"/"move", stays PASS.
dependency_known_maliciousObjective guardrailengine/rules/dependency_admission.pyAn installed package name (from a pip/npm/cargo/gem/go/... install command) exactly matches a name on Doberman's bundled known-malicious list, so the rule hard-blocks the install in every mode.
dependency_name_typosquatObjective guardrailengine/rules/dependency_admission.pyAn installed package name is exactly one character away (insertion, deletion, or substitution) from a name on Doberman's bundled popular-package list, and is not itself on that list, so it requires authentication before installing (a possible typosquat/slopsquat).

Notes

  • Match on the enum name in automation. Values are stable strings, but the name is what the code actually branches on.
  • Evidence may be redacted before it reaches a log or a UI. Reason codes are the machine-stable half of explainability; the explanation string on a Decision is the human-readable half.
  • When you add a ReasonCode, update this table in the same PR. tests/unit/test_reason_codes_documented.py fails the build otherwise.