mpfaffenberger / mpfaffenberger/code_puppy

destructive_command_guard and force_push_guard are whole-file structural clones — extract a shared command-guard framework

Open
#454 0 comments 0 reactions 0 assignees View on GitHub

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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.