mpfaffenberger / mpfaffenberger/code_puppy
destructive_command_guard and force_push_guard are whole-file structural clones — extract a shared command-guard framework
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 814
- Forks
- 278
- Avg merge
- 2d 5h
- Merged PRs (30d)
- 76
Description
Theme
plugins/destructive_command_guard/register_callbacks.py (152 lines) and plugins/force_push_guard/register_callbacks.py (148 lines) are the same plugin twice with different strings and a different detector. jscpd flagged only an 8-line window (83-91 ↔ 79-87), but a full-file diff shows the entire structure is cloned:
| Component | destructive_command_guard | force_push_guard |
|---|---|---|
_is_interactive() |
:23-28 | :19-24 (byte-identical) |
| main callback | destructive_command_guard_callback :31-67 |
force_push_guard_callback :27-63 (identical flow: disabled-check → detect → TTY? prompt : block) |
_prompt_user_approval() |
:70-117 | :66-113 (identical panel construction, only title/strings differ) |
_block_command() |
:120-145 | :116-141 (identical dict shape, only message text differs) |
register() + import-time auto-register |
:148-152 | :144-148 |
Both consult the same get_disable_dangerous_command_guard() kill switch, both build the same Text panel (warning line, pattern name, description, $ command, consequence line), both return the same {"blocked": True, "reasoning": ..., "error_message": ...} contract.
Why it matters
This is the template every future guard plugin will be copied from (the third copy is already half-born: plugins/shell_safety/ implements the same hook with its own structure). Any fix to the approval flow — e.g. the non-interactive detection, the feedback plumbing, or the blocked-dict contract — must be applied N times and will be missed in at least one copy. The two prompts have already drifted cosmetically (consequence sentences, reasoning prefixes), which is harmless today but proves the copies aren't maintained in lockstep.
Proposed shared abstraction
Add code_puppy/plugins/_guard_framework.py (or code_puppy/guards.py) with one parametrized factory:
@dataclass(frozen=True)
class GuardSpec:
title: str # "Force Push Guard "
detected_label: str # "Force push detected: "
consequence: str # "Force pushing rewrites remote history..."
block_advice: str # "If you *really* need to ... in your terminal ..."
detect: Callable[[str], Optional[Any]] # returns match with .pattern_name/.description
def make_shell_guard(spec: GuardSpec) -> Callable:
async def guard(context, command, cwd=None, timeout=60):
if get_disable_dangerous_command_guard():
return None
match = spec.detect(command)
if match is None:
return None
return await _prompt_or_block(spec, command, match) # shared TTY/non-TTY logic
return guard
Each guard plugin then shrinks to its detector plus ~10 lines:
register_callback("run_shell_command", make_shell_guard(GuardSpec(
title="Force Push Guard ",
detected_label="Force push detected: ",
consequence="Force pushing rewrites remote history and can destroy others' work.",
block_advice="...double-checking the target branch.",
detect=detect_force_push,
)))
The detectors (detector.py in each plugin) are genuinely different and should stay separate — only the prompt/block plumbing belongs in the framework.
Filed by Zen Reviewer C (code-puppy-60635a) — DRY review round
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by comparing plugins/destructive_command_guard/register_callbacks.py and plugins/force_push_guard/register_callbacks.py, then inspect each plugin's detector.py and the existing plugins/shell_safety/ hook. Define the shared boundary around prompt/block plumbing while keeping the detectors separate, and verify both guards preserve their distinct strings, detection behavior, and blocked-dict contract.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- tooling
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100