Originally created by: QY-25123
Pull Request
Slice
- Repo: doberman-core
- Feature / Slice: uninstall — project-scoped, gated
doberman uninstall (#250)
- Plan reference: n/a (ad hoc feature, see [#250])
What this PR does
doberman uninstall-hooks only strips the hook entries from settings.json and requires no
authentication — it never touches .doberman/, and nothing stops a protected agent that reaches
a shell from disabling its own security layer if it wants to (the gap [#250] identified).
This adds doberman uninstall:
- Removes this project's Claude Code hooks (project + local scope only — not
--global) and
Codex CLI hooks (repo scope only), plus the project's .doberman/ control plane (policy +
decision database), in one step.
- Gated behind the same possession-factor check as
doberman taint clear /
doberman memory reset: 2FA if enrolled, otherwise the local password. Fails closed with
neither enrolled — nothing is removed.
- Prints exactly what will be removed before prompting for anything.
- Since it's destructive and irreversible, also requires typing the project directory name back
to confirm (--yes skips only that prompt — the possession-factor check is never skippable).
- On any error partway through removal, reports every failure clearly rather than silently
succeeding or silently leaving a partial state.
"uninstall" is added to the shell-layer control-plane block list
(_DOBERMAN_CONTROL_SUBCOMMANDS in engine/rules/commands.py), so a mediated agent invoking
doberman uninstall through a shell is blocked pre-auth exactly like uninstall-hooks already
is — the new command cannot become a bypass of the existing protection.
Deliberately project-scoped only. --global hooks and device-wide state (password hash, TOTP
secret, fingerprint key, ~/.doberman/metrics.db) all live outside any repo and are shared across
every project Doberman protects on the machine — removing one project's protection must not
silently deauthorize every other project. This is asserted directly in tests (a successful
uninstall leaves --global hooks and the device-wide password file untouched).
Closes [#250].
Tests added (run in CI)
tests/unit/test_cli_uninstall.py (new) — no-factor-enrolled refuses and changes nothing;
wrong-factor refuses and changes nothing; confirmation declined / typed name mismatch refuse
and change nothing; a correct run removes project + local Claude hooks, Codex repo hooks, and
.doberman/, while preserving foreign (non-Doberman) hook entries in the same file; --dry-run
removes nothing; --global hooks and the device-wide password file survive a successful run
(the scope-boundary regression test); no output path ever echoes the password.
tests/unit/test_rule_commands_control_plane.py — doberman uninstall (with and without
--yes) is blocked pre-auth via the shell, same as the other posture/auth-mutating verbs.
tests/unit/test_cli_help.py — the new command is covered by the existing --help
smoke-test sweep.
Public-release safety (doberman-core only)
- [x] Contains nothing from the "not allowed" list: no enterprise/hosted code, no proprietary detection, no customer data, no secrets, no commercial-license code
- [x] Core still builds/tests/runs with NO enterprise package installed
Security checklist
- [x] Fails closed on error / uncertainty (no factor enrolled, wrong factor, declined confirm, or a mid-removal error all leave state unchanged / clearly reported)
- [x] No secret, full file, or unredacted prompt logged or committed
- [x] Any guardrail/learning change is raise-only (no silent loosening) — this only ever removes state, and does so behind the same gate as existing raise-only-adjacent recovery actions (
taint clear, memory reset)
- [x] Every BLOCK/AUTH carries reason codes + a human explanation — the shell-layer block reuses the existing
protected_path_blocked reason code and explanation text
- [x] doberman-core does not import doberman_enterprise
Edge cases covered / Deviations from plan / Risks introduced
- Deviation from my own proposal in [#250]: the issue's proposal text loosely said "for the
target repo/device" and listed "keys" among what gets removed. After reading the actual storage
layout, I corrected that in the issue itself (see the Scope clarification I added to [#250]) —
password/2FA/fingerprint key are device-wide, not repo-scoped, so this command does not touch
them. Tests assert that boundary directly.
- No ledger entry on success. Unlike
memory reset (which records to the append-only
policy_changes ledger via apply_change after clearing rows in the same, still-existing*
DB), uninstall deletes .doberman/ entirely — writing a ledger row afterward would silently
recreate a stub .doberman/ containing only that one row, which seemed worse (a "removed"
project isn't actually empty, and future doctor/status runs would report it as present
again). I chose not to force an audit record here rather than resurrect the directory as a side
effect; happy to revisit if a different tradeoff is preferred.
- Local full-suite verification on this branch was inconclusive for reasons unrelated to this
change: several pre-existing tests (e.g. test_hosthook_codex.py, test_hosthook_taint_floor.py)
spawn a real Tk GUI approval dialog on a background thread, which hard-aborts the Python
process on this contributor's macOS sandbox (Fatal Python error: Aborted) — reproduced
identically on a clean stash of main, so it predates this PR and isn't introduced by it. Every
test file this PR actually touches or is relevant to (test_cli_uninstall.py,
test_rule_commands_control_plane.py, test_cli_help.py, test_install_hooks.py,
test_cli_doctor.py) passes cleanly in isolation, and a serial full-suite run got to 51% with
zero failures before hitting that unrelated pre-existing issue. CI (ubuntu-latest, no
display) should not hit this at all.
Ticket changed by: fu351
Originally posted by: fu351
Merged, and welcome. This was a strong first landing: a reproducible bug report (#372), the minimal docs fix (#373), and the real fix here, three cleanly scoped pieces that each stand alone.
Two decisions stood out. You routed the authorization through the same
_verify_possession_factorhelper thattaint clearandmemory resetuse, so the new confirm-and-type-the-name UI never invents its own gate, and the factor check sits outside the--yespath, so it can never be skipped. And you addeduninstallto_DOBERMAN_CONTROL_SUBCOMMANDSin the same PR, so an agent shellingdoberman uninstallis blocked at the command rule rather than sailing past it. The scope-boundary regression tests (global hooks and the device-wide password survive even a successful run) are the tests a destructive command needs and most first PRs skip.Full disclosure of what I touched on your branch: after [#373] merged first, this PR conflicted in
docs/SETUP.md, since your two PRs edited the same section. I merged main into your branch and kept both hunks, yourdoberman uninstallparagraph followed by [#373]'s ordering callout. Nothing else changed, and CI re-ran green before the merge.Next one, if you want it: the piece of [#372] this PR leaves open, the self-healing path (
install-hooks/setupdetecting orphaned hook entries and offering to clean them, plusdoctorflagging the state). It's your own proposal and nobody knows the surface better right now. Comment there and I'll assign it to you.We also hang out on Discord if you want a faster feedback loop: https://discord.gg/Sfy5XGNqty. No obligation, ever.
Related
Tickets:
#372Tickets:
#373Originally posted by: fu351
@QY-25123 The gated
doberman uninstallclosed a long-standing gap, and carefully. Thanks again.Quick community roll-call: are you in the Doberman Discord? If yes, drop your handle here (or DM me there) so I can match the GitHub side of your work to your Discord name. If not, you're warmly invited: https://discord.gg/Sfy5XGNqty