<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Alan Fu</title>
    <description>The latest articles on DEV Community by Alan Fu (@doberman).</description>
    <link>https://dev.to/doberman</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4107353%2F22ccf416-5d07-4ad6-b079-b4e82f3e4462.png</url>
      <title>DEV Community: Alan Fu</title>
      <link>https://dev.to/doberman</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/doberman"/>
    <language>en</language>
    <item>
      <title>Make an agent's security policy fit the repo, and make its decisions visible</title>
      <dc:creator>Alan Fu</dc:creator>
      <pubDate>Thu, 24 Sep 2026 22:00:00 +0000</pubDate>
      <link>https://dev.to/doberman/make-an-agents-security-policy-fit-the-repo-and-make-its-decisions-visible-5a6m</link>
      <guid>https://dev.to/doberman/make-an-agents-security-policy-fit-the-repo-and-make-its-decisions-visible-5a6m</guid>
      <description>&lt;p&gt;A useful security policy needs to describe the project you're working on. A migration, a generated file and an ordinary source edit can carry different consequences, even when the same coding agent performs them.&lt;/p&gt;

&lt;p&gt;Doberman has a few ways to make those boundaries explicit and inspect the decisions they produce. This week's focus is customization and visibility.&lt;/p&gt;

&lt;p&gt;A repo-root doberman.policy.yaml puts supported policy settings in git. You can review a protected-path change in a pull request, alongside its reason. The file is resolved on the action path. A weakening change still needs the explicit policy-file acceptance path and human verification; editing YAML doesn't quietly remove the protection already in force.&lt;/p&gt;

&lt;p&gt;For behavior beyond the supported policy settings, the plugin interfaces let you attach your own implementation. A rule or detector is a Python package advertising the documented entry point, and it must be explicitly enabled. Rules use the objective guardrail; detectors use the subjective guardrail on the adaptive MCP path. The extension guide includes a worked detector example. This is code you need to review and test, rather than a promise that any natural-language rule can be pasted in safely.&lt;/p&gt;

&lt;p&gt;Once the policy is in place, you need to understand its effect. The local dashboard shows the decision feed. On the MCP proxy path it can also present actionable pending approvals. Host-hook decision rows appearing in the feed do not establish that those approvals use the dashboard, so a demonstration should identify the integration. The terminal UI and log explanations provide another way to inspect a decision.&lt;/p&gt;

&lt;p&gt;Deletion safeguards make that visibility concrete. The host-hook flow can show a bounded preview of the effect and re-check it before forwarding an approved deletion. A detected change prevents the changed effect from inheriting the earlier approval. It isn't an atomic filesystem transaction, and any demonstration should use the exact released version it claims to show.&lt;/p&gt;

&lt;p&gt;For the adaptive MCP path, memory seed accepts validated allowed-action traces to initialize persisted baseline statistics. It doesn't switch off the deterministic security floor. The in-memory Half-Space-Trees model still starts cold in a new proxy process, and native Claude/Codex hooks don't automatically run that full adaptive stack. Those boundaries matter when describing what has been warmed.&lt;/p&gt;

&lt;p&gt;The quieter tools are useful too. A scan can inspect the configured surface, doctor can point to wiring problems, and policy history can help explain a change. A clean configuration report should be followed by a real interception check before relying on the integration.&lt;/p&gt;

&lt;p&gt;I built Doberman so the boundaries and decisions can be inspected. I would start with one repo, one policy requirement and one harmless verification case, then expand from something that works.&lt;/p&gt;

&lt;p&gt;Doberman is open source, and the extension and setup guides describe the supported paths. What would you want to attach first: your own protected paths, a custom rule, or an audit integration?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/DobermanCore/Doberman-Core" rel="noopener noreferrer"&gt;https://github.com/DobermanCore/Doberman-Core&lt;/a&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>security</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Making agent approvals easier to live with</title>
      <dc:creator>Alan Fu</dc:creator>
      <pubDate>Thu, 17 Sep 2026 22:00:00 +0000</pubDate>
      <link>https://dev.to/doberman/making-agent-approvals-easier-to-live-with-4oo</link>
      <guid>https://dev.to/doberman/making-agent-approvals-easier-to-live-with-4oo</guid>
      <description>&lt;p&gt;A guardrail becomes difficult to keep enabled when every interruption sends you looking for a phone or typing another code. I've experienced that friction using coding agents, and I've been working on the ways Doberman asks for a decision and helps you recover afterward.&lt;/p&gt;

&lt;p&gt;There are several changes worth trying together. They solve different parts of the workflow, so it helps to know where each one applies.&lt;/p&gt;

&lt;p&gt;On a Windows laptop, Windows Hello can supply the proof for a supported two-factor action challenge. Once the method is available and enabled, the device verification flow can replace entering a TOTP code. The approval is still tied to the action in front of you. Hello doesn't replace every password or recovery gate in Doberman.&lt;/p&gt;

&lt;p&gt;When you're away from the laptop, ntfy can bring the pending action to your phone with Approve and Deny buttons. On supported action challenges, a tap can provide the confirmation or second factor. It covers Claude Code and Codex hooks, Cursor hooks and the MCP proxy; OpenClaw uses its own approval flow. The notification goes through the ntfy server you configure, so the setup guide explains the data that leaves the machine and the self-hosting option.&lt;/p&gt;

&lt;p&gt;Preference order matters when both methods are enabled. An available Hello method ahead of ntfy can handle a two-factor challenge before the phone is used. Checking the selected method is part of setting this up, rather than assuming every installed feature is active.&lt;/p&gt;

&lt;p&gt;For eligible repeats, approval memory remembers the exact action for five minutes after a qualifying proof. A repeat can use a shorter confirmation. It still asks, and clicking again doesn't extend the original window. Changed arguments, targets or security context can require fresh proof. Destructive and other high-risk cases are excluded.&lt;/p&gt;

&lt;p&gt;There is also an explicit recovery route for sticky taint. After reviewing the situation, you can run doberman taint clear from your own terminal and verify with TOTP if enrolled, otherwise your Doberman password. It clears the repo's taint state across sessions without requiring an agent restart. A later sensitive read can taint it again. Hello and ntfy currently do not replace that recovery command's factor gate.&lt;/p&gt;

&lt;p&gt;If prompts still get in the way, doberman tune reports the AUTH reasons and approval history. It can propose a narrow grant for qualifying repeated role-boundary cases, but the report applies nothing automatically. Any acceptance follows the human verification path.&lt;/p&gt;

&lt;p&gt;These features are released capabilities. Whether they make a particular workflow comfortable needs a real session on the relevant host, including ordinary work and the actions that used to cause trouble. I would measure time spent answering, repeated proofs, recovery effort and whether you choose to leave protection enabled.&lt;/p&gt;

&lt;p&gt;Doberman is my open-source project. Which part of that workflow would make the biggest difference for you: approving locally, approving remotely, recovering from a flagged session, or understanding why a prompt appeared?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/DobermanCore/Doberman-Core" rel="noopener noreferrer"&gt;https://github.com/DobermanCore/Doberman-Core&lt;/a&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>automation</category>
      <category>security</category>
      <category>software</category>
    </item>
    <item>
      <title>Fail closed: what my AI guard dog does when it doesn't know</title>
      <dc:creator>Alan Fu</dc:creator>
      <pubDate>Thu, 10 Sep 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/doberman/fail-closed-what-my-ai-guard-dog-does-when-it-doesnt-know-282p</link>
      <guid>https://dev.to/doberman/fail-closed-what-my-ai-guard-dog-does-when-it-doesnt-know-282p</guid>
      <description>&lt;p&gt;Most guardrail code I've read optimises for the happy path. Classify the action, match a rule, return a verdict, done. The interesting cases are the other ones: the timeout, the malformed input, the rule that throws, the case nobody wrote a test for. What your guardrail does there is the whole security model, whether you designed it or not.&lt;/p&gt;

&lt;p&gt;Doberman's rule fits in one sentence. Any error, any uncertainty, any unhandled case, any approval prompt nobody answers, all resolve to deny. It never logs and continues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is harder than it sounds
&lt;/h2&gt;

&lt;p&gt;The easy version of a hook is a script that runs before a tool call and prints allow or deny. The default in a lot of host integrations is that if the hook crashes, the call goes through. That is fail open, and it means every bug in your guardrail is a bypass. Your agent doesn't need to jailbreak anything. It just needs to hit an input your script didn't expect.&lt;/p&gt;

&lt;p&gt;So the first thing I did was invert that. If the decision engine can't produce a verdict, the verdict is BLOCK. If a rule raises, BLOCK. If the policy file won't parse, BLOCK. It's annoying during development, because a bug stops your own work. That annoyance is the point. You find the bug in an afternoon instead of finding it in an incident report.&lt;/p&gt;

&lt;h2&gt;
  
  
  Silence is a deny
&lt;/h2&gt;

&lt;p&gt;The second case is the one people forget. An action gets an AUTH verdict, the approval dialog pops up, and nobody is at the keyboard. What happens?&lt;/p&gt;

&lt;p&gt;In Doberman the prompt times out to deny, and the log records it as a timeout, not as a denial. Those are different facts. "Nobody was there" and "someone said no" both stop the action, but you'll want to tell them apart later when you're reading the log and deciding whether the rule is too noisy.&lt;/p&gt;

&lt;p&gt;A prompt that times out to allow turns approval fatigue into an exploit. Wait long enough and everything passes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Raise-only, the companion rule
&lt;/h2&gt;

&lt;p&gt;Fail closed covers the moment of decision. Raise-only covers what happens over time. Guardrails and learned baselines can tighten on their own. They can never quietly loosen. Any permanent weakening goes through a human with a possession factor (TOTP if you've enrolled one, the local password otherwise) and lands in an append-only ledger.&lt;/p&gt;

&lt;p&gt;Put the two together and the system's default drift, under bugs, under load, under attack, is always toward more caution. That's the property I actually care about. Not that it catches everything today, but that it can't get worse without a person signing off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it on yourself
&lt;/h2&gt;

&lt;p&gt;If you run a coding agent with any kind of hook or wrapper, try this: make the hook crash on purpose and see whether the tool call goes through. It's a five-minute test and the answer is usually a surprise.&lt;/p&gt;

&lt;p&gt;Doberman is open source, Apache-2.0, pip install doberman-core then doberman setup. It's alpha and I'm building it alone, so if you find a case where it fails open, that's the bug report I want most.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/DobermanCore/Doberman-Core" rel="noopener noreferrer"&gt;https://github.com/DobermanCore/Doberman-Core&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What does your setup do when the guardrail itself breaks?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>opensource</category>
      <category>python</category>
    </item>
    <item>
      <title>Same curl, two verdicts: taint tracking for coding agents</title>
      <dc:creator>Alan Fu</dc:creator>
      <pubDate>Wed, 09 Sep 2026 19:17:11 +0000</pubDate>
      <link>https://dev.to/doberman/same-curl-two-verdicts-taint-tracking-for-coding-agents-367p</link>
      <guid>https://dev.to/doberman/same-curl-two-verdicts-taint-tracking-for-coding-agents-367p</guid>
      <description>&lt;p&gt;Here's the case that convinced me a static allowlist isn't enough for coding agents.&lt;/p&gt;

&lt;p&gt;An agent reads a .env file. Ten minutes later, in the same session, it runs a curl to a host it hasn't talked to before, and part of the request body happens to match a value from that file. Every individual step looks fine. Reading config is normal. Curl is normal. The problem is the combination, and the combination only exists across time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a sandbox sees
&lt;/h2&gt;

&lt;p&gt;A sandbox sees a process making a network call and checks whether that's allowed. It doesn't know what the process read earlier. It can't, that isn't its job. So the same curl command gets the same answer every time, and if outbound network is allowed at all, the secret leaves.&lt;/p&gt;

&lt;p&gt;This is what Simon Willison calls the lethal trifecta: private data, untrusted content, and a path to exfiltrate. A coding agent on your laptop usually has all three at once. You can't remove any of them and still get work done, so the decision has to happen per action, with the session's history in view.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Doberman does instead
&lt;/h2&gt;

&lt;p&gt;Doberman keeps session state. When the agent reads something secret-shaped, the session gets a raised floor on outbound actions. Nothing is blocked yet, but the bar for the next network call goes up. When an outbound value matches something the session already read, that's a hard BLOCK, and the reason code says why: secret-egress taint from the earlier read.&lt;/p&gt;

&lt;p&gt;Same curl, two verdicts. Fine in a clean session, blocked in one that just touched a secret. That's taint tracking, not an allowlist, and it's the part sandboxing alone can't do.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't do
&lt;/h2&gt;

&lt;p&gt;I'll say the limits before someone else does. The match is on shape and value, not intent. An agent that encodes the secret first, or splits it across two calls, is a different attack, and the benchmark corpus has a category for encoded exfiltration precisely because it's harder. The numbers for that category are in docs/BENCHMARKS.md, misses first.&lt;/p&gt;

&lt;p&gt;The log never stores the secret either. It stores an HMAC-SHA256 fingerprint of what was involved, so you can prove "a secret-shaped thing was present and this happened" without the log becoming a second copy of the secret.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run both
&lt;/h2&gt;

&lt;p&gt;Sandboxes limit where the damage lands. Something still has to decide whether the action happens at all. I run both, and I mean that every time I say it.&lt;/p&gt;

&lt;p&gt;pip install doberman-core, doberman setup, then doberman demo to watch the .env-then-curl case get blocked on your own machine.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/DobermanCore/Doberman-Core" rel="noopener noreferrer"&gt;https://github.com/DobermanCore/Doberman-Core&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What's the one action in your agent's session you'd want judged by what came before it?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How I benchmark an agent guardrail without lying to myself</title>
      <dc:creator>Alan Fu</dc:creator>
      <pubDate>Sun, 06 Sep 2026 07:20:53 +0000</pubDate>
      <link>https://dev.to/doberman/how-i-benchmark-an-agent-guardrail-without-lying-to-myself-5b83</link>
      <guid>https://dev.to/doberman/how-i-benchmark-an-agent-guardrail-without-lying-to-myself-5b83</guid>
      <description>&lt;p&gt;It would be easy to write a benchmark for Doberman that makes Doberman look good: pick the attacks it already stops, skip the ones it doesn't, round up. I've tried to build the opposite of that - a methodology that reports the numbers whether they're flattering or not, and a results page (&lt;code&gt;docs/BENCHMARKS.md&lt;/code&gt;) that lists what it missed before what it caught.&lt;/p&gt;

&lt;h2&gt;
  
  
  The metrics, and why there are three of them
&lt;/h2&gt;

&lt;p&gt;Doberman is scored as a decision function over tool-call cases, not by driving a live LLM agent. Each case - a tool call or an ordered sequence - is labeled benign or attack, replayed through the real decision engine, and the verdict (PASS/AUTH/BLOCK) is recorded. Deterministic, no model called, reproducible from a cold clone.&lt;/p&gt;

&lt;p&gt;Three numbers matter, and none of them alone tells the truth:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ASR (attack success rate)&lt;/strong&gt; - the fraction of attacks that reach the tool. A hard BLOCK counts as stopped, but &lt;code&gt;AUTH&lt;/code&gt; also counts as "not silently succeeded" in plain ASR, which flatters the score.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;asr_strict&lt;/strong&gt; - the same measurement, but only a hard BLOCK counts as stopped. An AUTH is not a block. A human still has to say no.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FPR (false-positive rate)&lt;/strong&gt; - the fraction of &lt;em&gt;benign&lt;/em&gt; cases that get friction (AUTH or BLOCK). A guardrail that blocks everything has a perfect ASR and a useless FPR.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Report ASR alone and you can make a leaky guardrail look airtight, because every AUTH counts as a win. That's why every result in the docs reports both numbers side by side, plus operator metrics (&lt;code&gt;effective_asr_deny&lt;/code&gt;/&lt;code&gt;_approve&lt;/code&gt;, &lt;code&gt;asr_under_fatigue&lt;/code&gt;) that model what happens if the human on the other end of an AUTH prompt just rubber-stamps it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure case, published first
&lt;/h2&gt;

&lt;p&gt;On the synthetic suite (3 attacks, 3 benign - a deterministic CI smoke gate, not a coverage claim), Doberman converts every attack from a silent bypass into an AUTH: &lt;code&gt;asr&lt;/code&gt; drops from 1.00 to 0.00. &lt;code&gt;asr_strict&lt;/code&gt; stays at 1.00, because none of the three get a hard BLOCK - all three route to a human decision. If that human approves, &lt;code&gt;effective_asr_approve&lt;/code&gt; is 1.00: nothing was stopped. If they deny, &lt;code&gt;effective_asr_deny&lt;/code&gt; is 0.00: everything was stopped. &lt;strong&gt;A human who approves is not protected.&lt;/strong&gt; AUTH is a leash, not a wall, and I'd rather that be the first thing you read about the synthetic suite than the last.&lt;/p&gt;

&lt;h2&gt;
  
  
  The labeled corpus
&lt;/h2&gt;

&lt;p&gt;The synthetic suite is a smoke test. The real per-category picture comes from a 158-row labeled fixture (&lt;code&gt;tests/corpus/detection_corpus.jsonl&lt;/code&gt;) across seven categories: injection, exfiltration, secrets, destructive, encoded, dependency, and benign. Run it with &lt;code&gt;python -m tests.benchmarks.run --suite corpus --corpus&lt;/code&gt;, and it reports TPR (AUTH or BLOCK counts as mitigation), &lt;code&gt;tpr_strict&lt;/code&gt; (BLOCK only), FPR, and precision, per category.&lt;/p&gt;

&lt;p&gt;The published run (2026-09-02: 125 attack / 33 benign rows) isn't filtered to cases the engine wins. Natural-language injection with no matching command shape scores &lt;strong&gt;TPR 0.00&lt;/strong&gt;, flat, documented as a structural gap: the objective deterministic layer can't see intent, only shape, and it says so instead of hiding the category. Exfiltration is mode-gated: balanced mode deliberately lets a bare unknown host through (TPR 0.375) while strict mode steps it up (1.00). Secrets score TPR 1.00 but &lt;code&gt;tpr_strict&lt;/code&gt; only 0.43, meaning most of that mitigation is still AUTH, not BLOCK. Overall, balanced mode lands at TPR 0.77 but &lt;code&gt;tpr_strict&lt;/code&gt; 0.08 (strict mode: 0.81 and 0.36), which is the same "AUTH is a leash" honesty at corpus scale: almost all of the real mitigation on this fixture is a human being asked, not a hard stop. Benign FPR across the whole corpus is 0.00, and precision is 1.00, so the objective layer isn't buying that TPR by hassling legitimate traffic - but I'd rather you read the low &lt;code&gt;tpr_strict&lt;/code&gt; than stop at the high TPR.&lt;/p&gt;

&lt;p&gt;Every attack row's floor is calibrated to what the engine &lt;em&gt;actually does today&lt;/em&gt;, not an aspiration, and CI gates on any row dropping below its documented floor. A generator that refuses to let a shipped floor quietly get worse is doing the same "raise-only" thing the runtime does, just for the test suite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Poisoning, across sessions, not just within one
&lt;/h2&gt;

&lt;p&gt;Static ASR says nothing about a patient attacker teaching the adaptive layer to trust something dangerous one small step at a time, spread across separate sessions so no single session looks abnormal. There's a dedicated eval for exactly that: it simulates a proxy restart between sessions (so the in-memory drift detector re-warms cold, exactly like production), and measures the &lt;strong&gt;admitted poisoning rate&lt;/strong&gt; - the fraction of dangerous targets an attacker can normalize using only actions the real engine actually let through with a PASS. No cheating by injecting pre-approved actions.&lt;/p&gt;

&lt;p&gt;The admitted poisoning rate is &lt;strong&gt;0&lt;/strong&gt;. The reason is structural: teaching the baseline to trust a dangerous action requires &lt;em&gt;allowed observations of that exact action&lt;/em&gt;, and those are precisely the observations the engine steps up and refuses to silently learn from. The lethal-trifecta floor underneath it is score-independent, so even a &lt;code&gt;worst_case&lt;/code&gt; attacker who's already defeated the approval gate and gets everything learned can't flip the floor's verdict by grinding the score down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it on your own corpus
&lt;/h2&gt;

&lt;p&gt;None of this is a leaderboard. There's no competitor comparison in the docs, on purpose. If you've got a labeled corpus of your own - attack shapes specific to your stack, your MCP tools, your CI pipeline - the harness runs on any labeled tool-call fixture, not just the one shipped in the repo. I'd like to see where it holds and, more usefully, exactly where it doesn't.&lt;/p&gt;

&lt;p&gt;Full methodology and the raw tables: &lt;code&gt;docs/BENCHMARKS.md&lt;/code&gt; in &lt;a href="https://github.com/DobermanCore/Doberman-Core" rel="noopener noreferrer"&gt;https://github.com/DobermanCore/Doberman-Core&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>security</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Prompt filters are advisory. Enforce on the execution path.</title>
      <dc:creator>Alan Fu</dc:creator>
      <pubDate>Thu, 03 Sep 2026 20:46:10 +0000</pubDate>
      <link>https://dev.to/doberman/prompt-filters-are-advisory-enforce-on-the-execution-path-kd4</link>
      <guid>https://dev.to/doberman/prompt-filters-are-advisory-enforce-on-the-execution-path-kd4</guid>
      <description>&lt;p&gt;Simon Willison calls it the lethal trifecta: private data, untrusted content, and a way to exfiltrate, all in the same session. A coding agent on your laptop has all three by default. It reads your code (private data), it reads whatever a dependency's README or a fetched URL says (untrusted content), and it can run &lt;code&gt;curl&lt;/code&gt; (a way out). Once those three are true at once, the only question left is whether something in that untrusted content can talk the model into connecting them.&lt;/p&gt;

&lt;p&gt;Most of what gets called an "AI guardrail" tries to stop that by reading the prompt or the model's output and flagging what looks dangerous. That's advisory. It runs before or after the model has already decided what to do, and a sufficiently clever piece of injected text just needs to talk the model past the filter, not past a real barrier. The filter's verdict is a suggestion the model is free to ignore, because nothing forces the model's actual tool call to go through it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put the checkpoint where the action actually happens
&lt;/h2&gt;

&lt;p&gt;Doberman's answer is boring on purpose: don't inspect the conversation, inspect the call. Every tool call a coding agent makes - shell command, file write, MCP call - gets normalized into one object and routed through a decision engine before it executes. The model can be talked into requesting anything it wants. What it can't do is make the call happen without going through that one chokepoint, because there is no other path to the tool.&lt;/p&gt;

&lt;p&gt;Two properties turn "a chokepoint exists" into something you can actually rely on:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fail closed.&lt;/strong&gt; Any error, any unhandled case, any uncertainty resolves to a denial. That has to include silence: an approval prompt nobody answers is not a maybe, it's bounded by a hard deadline (2 minutes for a desktop dialog, 20 minutes as the whole-challenge backstop) and then denies, logged distinctly as &lt;code&gt;timeout&lt;/code&gt; so you can tell it apart from an explicit &lt;code&gt;denied&lt;/code&gt; later. Agents run unattended a lot of the time. A guardrail that quietly lets things through when it's confused, or when nobody's watching, isn't a guardrail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Raise-only.&lt;/strong&gt; The guardrail and its learning can tighten automatically as it sees more of your workflow. It can never loosen itself. Any permanent weakening of policy needs a possession-factor-gated human approval (a TOTP code if you've enrolled one, otherwise the local password) and lands in an append-only ledger. That matters because the same channel an attacker uses to try to get an action approved is the channel that would let them quietly turn the guardrail down, if loosening were as easy as tightening.&lt;/p&gt;

&lt;p&gt;Put those together and you get an actual guarantee instead of a probability: a blocked action never runs, full stop, regardless of what the model was convinced to try, because the block doesn't depend on catching the &lt;em&gt;reasoning&lt;/em&gt;, it depends on stopping the &lt;em&gt;call&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The taint floor
&lt;/h2&gt;

&lt;p&gt;Fail-closed and raise-only apply within a single action. The taint floor is what carries risk across actions in the same session. Once a session has read something that looks like a secret, every outbound action after that gets evaluated with a raised floor, and if an outbound value actually matches a secret the session read earlier, it's blocked outright, not authed - blocked. This is the direct answer to the trifecta: reading the secret doesn't get punished, trying to move it out afterward does, and the system doesn't need to guess intent to catch it, it just needs to remember what this session has already touched.&lt;/p&gt;

&lt;h2&gt;
  
  
  The control plane protects itself
&lt;/h2&gt;

&lt;p&gt;None of this means anything if the agent can edit its own leash. Doberman's own config, hooks, and policy files sit inside the boundary it enforces: the protected agent cannot edit them to let itself out. If an attacker's plan requires disabling or reconfiguring the guardrail as a step, that step is itself a gated action, not a side door.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sandbox or authorization? Both.
&lt;/h2&gt;

&lt;p&gt;A sandbox answers a different question than an authorization layer does, and conflating them is where a lot of "we're covered" confidence comes from. A sandbox limits where damage lands if an action executes: a container boundary, a restricted filesystem, a network namespace. It doesn't know or care why the action is happening. Authorization decides whether the action happens at all, with context. The same &lt;code&gt;curl&lt;/code&gt; call to the same host can be a PASS in a clean session and a BLOCK in a session that just read a &lt;code&gt;.env&lt;/code&gt; file, because the decision isn't about the command in isolation, it's about the command plus what this session has already done. A sandbox can't see that; it has no session memory to raise a floor with.&lt;/p&gt;

&lt;p&gt;Run both. They're not competing for the same job. The sandbox bounds the blast radius of whatever gets through. The authorization layer decides what gets through in the first place, with the accumulated context a static sandbox boundary never has.&lt;/p&gt;

&lt;p&gt;Doberman is open source (Apache-2.0), alpha, and defense-in-depth, not airtight, on purpose - no single rule here is being sold as a guarantee except the two structural ones above. Code and docs: &lt;a href="https://github.com/DobermanCore/Doberman-Core" rel="noopener noreferrer"&gt;https://github.com/DobermanCore/Doberman-Core&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>llm</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Doberman: Stops your AI before it goes rogue</title>
      <dc:creator>Alan Fu</dc:creator>
      <pubDate>Thu, 03 Sep 2026 06:09:56 +0000</pubDate>
      <link>https://dev.to/doberman/doberman-stops-your-ai-when-it-goes-rogue-2c69</link>
      <guid>https://dev.to/doberman/doberman-stops-your-ai-when-it-goes-rogue-2c69</guid>
      <description>&lt;p&gt;I built Doberman because a Claude code made me lose a hackathon. It got stuck on a bug and decided the best fix was just deleting the entire database, 5 minutes before demo time, and there was nothing standing between "the agent decided" and "the agent did it." I didn't win that hackathon.&lt;/p&gt;

&lt;p&gt;Doberman is local-first, open-source (Apache-2.0), and it sits on the execution path between your coding agent and its tools. Every shell command, every file write, every MCP call gets exactly one verdict before it's allowed to run: PASS, AUTH, or BLOCK. Here's the whole setup, in two commands, and what it actually looks like when it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;doberman-core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the package. Python 3.11+, and it ships as &lt;code&gt;doberman-core&lt;/code&gt; on PyPI (the bare &lt;code&gt;doberman&lt;/code&gt; name belongs to an unrelated, abandoned project; the CLI command is still &lt;code&gt;doberman&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Wire it up
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;doberman setup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the wizard. It detects which agents you have installed (Claude Code via hooks, Codex CLI experimentally, Claude Desktop/Cursor/any MCP client via a transparent proxy, and OpenClaw natively), asks which ones you want guarded, picks a strictness mode, tunes the guardrails, wires the hooks, and then asks about telemetry. It finishes with a health-check pass and, if you wired a hooks-based host, offers to run a scripted attack right there so you can watch it work.&lt;/p&gt;

&lt;p&gt;For Claude Code specifically, this is the recommended path: it wires a &lt;code&gt;PreToolUse&lt;/code&gt; hook that gates every built-in and MCP tool call before it runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What PASS / AUTH / BLOCK actually mean
&lt;/h2&gt;

&lt;p&gt;Every action out of your agent resolves to one of three verdicts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PASS&lt;/strong&gt; - routine work, straight through, no friction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AUTH&lt;/strong&gt; - sensitive enough to pause and ask you. Repeat the exact same action within five minutes and it re-prompts with a one-click confirm instead of the full challenge (never for anything destructive).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BLOCK&lt;/strong&gt; - dangerous, stopped cold before it reaches the tool.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last part is the whole point. This isn't a prompt filter reading what the model said it would do: it's on the execution path, so a blocked call literally never runs, no matter how the model was talked into requesting it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch it happen
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;doberman demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This replays a scripted rogue-agent sequence through the real decision engine: a secret exfiltration, an &lt;code&gt;rm -rf&lt;/code&gt;, a force push to a protected branch, a smuggled-token egress, and a &lt;code&gt;.env&lt;/code&gt; read, all blocked. Then it hands you a pending approval for a high-risk SSH-trust-file write and waits for a human to deny it. Nothing here touches a real tool or downstream server: it's the same engine your actual traffic runs through, just fed a script. &lt;code&gt;--fast&lt;/code&gt; skips the pacing, &lt;code&gt;--mode &amp;lt;name&amp;gt;&lt;/code&gt; runs it under a different strictness setting, &lt;code&gt;--quiet&lt;/code&gt;/&lt;code&gt;-q&lt;/code&gt; keeps the summary but drops the narration.&lt;/p&gt;

&lt;p&gt;If you've also got the dashboard installed (&lt;code&gt;pip install 'doberman-core[dash]'&lt;/code&gt;, then &lt;code&gt;doberman dash&lt;/code&gt;), run the demo next to it and watch the same five blocks land in a live feed as they happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  When something gets blocked and you want to know why
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;doberman log &lt;span class="nt"&gt;--why&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The decision log is local SQLite, redacted: verdicts, reason codes, and HMAC-SHA256 fingerprints, never raw secrets or full payloads. &lt;code&gt;--why&lt;/code&gt; turns the reason codes into a plain-language explanation. If you'd rather browse interactively, &lt;code&gt;pip install "doberman-core[tui]"&lt;/code&gt; then &lt;code&gt;doberman tui&lt;/code&gt; gives you the same rows with a keyboard-driven why panel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning telemetry off
&lt;/h2&gt;

&lt;p&gt;Anonymous usage counts (command names and daily totals, never paths, prompts, or secrets) are on by default. Turn them off with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;doberman telemetry off
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or set &lt;code&gt;DO_NOT_TRACK=1&lt;/code&gt; in your environment before you ever run a command, if you'd rather it never mint an id in the first place.&lt;/p&gt;

&lt;p&gt;It's alpha software with a real test suite behind it (2,000+ tests in CI as of v0.18.5), so if you hit a rough edge, submit an issue and I'll get to it within a week. I'd genuinely like to know what breaks it.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/DobermanCore/Doberman-Core" rel="noopener noreferrer"&gt;https://github.com/DobermanCore/Doberman-Core&lt;/a&gt;&lt;br&gt;
Docs: &lt;a href="https://docs.trydoberman.dev" rel="noopener noreferrer"&gt;https://docs.trydoberman.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>opensource</category>
      <category>claude</category>
    </item>
  </channel>
</rss>
