Menu

#375 feat(cli): add project-scoped, gated `doberman uninstall` (#250)

closed
nobody
None
2026-08-18
2026-08-14
Anonymous
No

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.pydoberman 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.

Related

Tickets: #250
Tickets: #372
Tickets: #373

Discussion

  • Anonymous

    Anonymous - 2026-08-15

    Ticket changed by: fu351

    • status: open --> closed
     
  • Anonymous

    Anonymous - 2026-08-15

    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_factor helper that taint clear and memory reset use, so the new confirm-and-type-the-name UI never invents its own gate, and the factor check sits outside the --yes path, so it can never be skipped. And you added uninstall to _DOBERMAN_CONTROL_SUBCOMMANDS in the same PR, so an agent shelling doberman uninstall is 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, your doberman uninstall paragraph 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/setup detecting orphaned hook entries and offering to clean them, plus doctor flagging 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: #372
    Tickets: #373

  • Anonymous

    Anonymous - 2026-08-18

    Originally posted by: fu351

    @QY-25123 The gated doberman uninstall closed 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

     

Log in to post a comment.