feat(hosthooks): recheck a delete's blast radius before execution (#649) - #665
Closed
harshitagrawal2O wants to merge 2 commits into
Closed
Conversation
…bermanCore#649) Before a risky delete runs, Doberman counts what it would touch, shows that to the human, and recounts right before execution, blocking if the count moved (ADR 0094's TOCTOU guard, reason code effect_set_diverged). That guard lived only in proxy/executor.py, so none of the four supported hosts had it, even though a host hook is how most agents actually reach a tool. `grep -rln effect_set_diverged src/doberman/` now lists hosthooks/hookio.py. The host-hook path computed no preview at all, so there was nothing a recheck could compare against and the challenge rendered no blast radius. This adds both halves in hookio.resolve_auth_result, the one place every adapter turns an approval into an execution: * before the challenge, compute the bounded effect set and attach it to the decision the prompters render, so the human approves a blast radius they can actually see; * after they answer, recompute the same operands and deny if the digest moved. Drift in either direction denies, and so does a known count degrading to unknown. A dynamic delete is `unknown` at both ends and cannot re-deny on dynamism alone. A recheck that raises denies too: a guard that cannot verify must not report success. Cost is unchanged for everything else. Both helpers early-out before any filesystem work for a non-delete action, and both imports are lazy, so the PASS/BLOCK hot path this module's docstring protects still pays nothing. Mutation-checked: forcing `diverged = False` turns six of the new tests red. OpenClaw is deliberately not covered and LIMITATIONS.md now says so: it defers an AUTH to its own /approve flow, which resolves outside this process, so there is no moment inside the hook where a recheck could run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fu351
pushed a commit
that referenced
this pull request
Sep 8, 2026
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> (cherry picked from commit 82989e8)
Collaborator
|
Merged via a landing branch, thanks @harshitagrawal2O! Putting the recheck in resolve_auth_result, the one chokepoint all three adapters already go through, means the next host inherits it for free, and a recheck that throws denying instead of passing is the fail-closed shape I want. Nothing changed in your code: I cherry-picked your commits onto main under your name because main now refuses merge commits and needs branches up to date, and put Closes #649 on the landing PR. Notes on #653 and #663 are coming today, both need a decision from you rather than a fix from me. Feel free to star the repo if it's been useful. |
This was referenced Sep 9, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pull Request
Slice
What this PR does
The issue's own reproduction, before and after:
It was bigger than "add a recheck." The host-hook path computed no preview at all —
grep -rn "compute_delete_effects\|delete_class_operands" src/doberman/hosthooks/came back empty. So there was nothing for a recheck to compare against, and the host-hook AUTH challenge was rendering no blast radius: the human was approving a delete with no idea of its size. Both halves are added here.Both live in
hookio.resolve_auth_result, which is where the issue points and where they belong — the one place all four adapters turn an approval into an execution, so no adapter can be left out by accident:What denies, and what does not
unknownrm -rf $(...)) —unknownat both endsrepo_root, so no baselineDrift in either direction denies: a shrunken effect set is still not the approved one.
Cost
Unchanged for everything that is not a delete.
_delete_operandsreturns(None, False)immediately for any non-command-bearing action or non-delete command, so no filesystem work is ever reached on a non-delete AUTH — asserted by a test that makescompute_delete_effectsraise if it is called. Both imports are lazy, so the PASS/BLOCK hot path this module's docstring protects still pays nothing at import time. The operand list is parsed once and reused for the recheck, matching the proxy's own M1 note.Tests added (run in CI)
tests/unit/test_hosthook_delete_recheck.py— 12 tests:repo_root.Mutation-checked. Forcing
diverged = Falseturns six of the twelve red. Reverted before commit.Changelog
changelog.d/<PR>.<type>.mdfragment addedPublic-release safety (doberman-core only)
Security checklist
repo_root, or a non-delete action), which is today's behaviour rather than a new hole.effect_set_divergedplus a next step, matching how the proxy path's synthetic BLOCK is explained.Edge cases covered / Deviations from plan / Risks introduced
OpenClaw is deliberately not covered, and
docs/LIMITATIONS.mdnow says so precisely. That adapter hands anAUTHto OpenClaw's own/approveflow, which resolves outside this process — asopenclaw._record_history's existing docstring notes — so there is no moment inside the hook where a recheck could run. Claude Code, Codex, and Cursor all route throughresolve_auth_resultand are covered. The limitation entry's heading and closing paragraph were rewritten rather than deleted: the other half of that limitation (drift is only detectable when both counts are exact) is unchanged and still true.Risks, stated plainly:
action.targetis the command source here, where the proxy usescommand_line_from_arguments(arguments). The hook has no raw-args dict at this point, andaction.targetis what the destructive-command rule itself falls back to for command-bearing action types, so this matches the rule's own view of the command. A tool whose command lives only in structured args and never reachestargetwould get no preview and no recheck — the same as today.feat(storage): record the resolving auth path...), which also toucheshookio.py. Mechanical to resolve; happy to rebase once that one lands, or land this first and rebase that one — whichever you prefer.🤖 Generated with Claude Code