Skip to content

wgordon17/dev-guard

v1.70.0

Development environment policy enforcement: tool selection guard, commit validation, URL fetch guard, trust management, oc/kubectl introspection, subagent completion verification, decision persistence, anti-deferral enforcement, shared behavioral feedback, path hallucination guard, narrative comment guard, token efficiency guidance

Dev Guard Plugin

Development environment policy enforcement: tool selection guard, commit validation, URL fetch guard, trust management, oc/kubectl introspection, subagent completion verification, decision persistence, anti-deferral enforcement, shared behavioral feedback, path hallucination guard, narrative comment guard, token efficiency guidance.

Commands (2)

CommandDescription
/dev-guard:guard-statsQuery dev-guard audit database for trigger trends, trust candidates, and noise signals
/dev-guard:trustManage dev-guard trust rules — list, add, remove, or clear trusted rules

Hooks

PreToolUse: Tool Selection Guard

tool-selection-guard.py — Enforces tool and command best practices:

  • Native tool redirections — Redirects grep/find/cat/sed to Grep/Glob/Read/Edit tools
  • Python tooling — Enforces uv run/uvx over bare python/pip
  • Git safety — Blocks force pushes, branch deletions, commits to main, and other destructive operations
  • URL fetch guard — Blocks or redirects WebFetch, WebSearch domain filters, and curl/wget for authenticated services and bot-blocked public sites (extensible via URL_GUARD_EXTRA_RULES)
  • Command guard — User-defined command blocking rules (via COMMAND_GUARD_EXTRA_RULES)
  • Interactive command blocking — Prevents git rebase -i, git add -p, and other interactive commands
  • Plan mode redirect — Redirects EnterPlanMode to incremental-planning skill

PostToolUse: Commit Message Validation

validate-commit-message.sh — Bash(git commit:*)

  • Validates Conventional Commits format
  • Enforces present indicative tense ("adds" not "add")
  • Checks subject line length (<72 chars, warn >50)
  • Blocks emoji and meta-commentary
  • Exit 2 shows errors but commit already completed (PostToolUse limitation)

PreToolUse/PostToolUse: AskUserQuestion Decision Persistence

decision-persistence.py — Remembers Fix/Defer review decisions across sessions:

  • PreToolUse — Checks stored decisions for AskUserQuestion calls tagged with ▸dp: metadata and auto-answers via updatedInput when every question in the batch has a prior, non-stale decision
  • PostToolUse — Captures new Fix/Defer (or multi-option) decisions from the user's actual answers, fingerprinted by file + category + line + skill
  • Staleness detection — A code-hash window around the finding location invalidates a stored decision if the surrounding code has changed since it was made
  • Partial-batch limitation — Auto-answering requires every question in a batch to have a prior decision; a mixed batch passes through entirely (no partial auto-answer, since updatedInput requires permissionDecision to take effect for the whole call)

Stop: Quality Stop Gate

stop-hook.py — Fires on every Stop event, performing deterministic triage (loop guard, transcript parsing, git diff, signal detection, question classification) and delegating to an LLM evaluator when quality checks are warranted.

  • Zero-dependency fast triage — Per-session state, transcript tail-read, git diff hash comparison
  • LLM delegation — Only invokes LLM when write signals, completion claims, or research activity detected
  • Deferral detection — Regex patterns scan recent assistant messages for self-scoping deferral (v1/v2 framing, "future iteration," "out of scope") and fabricated user-deferral claims; LLM evaluator distinguishes active deferral from quoted references
  • Fail-open — All errors exit 0 (allow stop) to avoid blocking legitimate exits

SubagentStop: FixSummary Validation

subagent-stop-hook.py — Fires on every SubagentStop event. Validates FixSummary structural completeness when Fixer subagents stop.

  • Transcript-based detection — Tail-reads 512KB of the subagent transcript to find FixSummary JSON
  • Structural validation — Ensures findings_fixed, needs_input_items, and user_deferred arrays account for at least one finding
  • Loop guard — After 3 consecutive blocks for the same subagent, fails open (approves) to prevent infinite retries
  • Fail-open — All errors exit 0 (approve stop); best-effort enforcement only

SessionStart: Shared Behavioral Feedback

inject-reference.sh shared-feedback.md — Injects cross-project behavioral rules from references/shared-feedback.md into every session. Rules include anti-deferral guidance (no v1/v2 self-scoping, no fabricated user deferral, scope decisions belong to user).

SessionStart: Token Efficiency

inject-reference.sh token-efficiency.md — Injects a vendored, locally-patched copy of JetBrains/benjamin-plus-skill's token-efficiency ruleset from references/token-efficiency.md into every session. Rules cover recon batching, keyhole reads, dependency probing, treating the task's own check as the green signal, and pacing polling intervals.

How Hooks Work

Hooks execute automatically when enabled:

  1. Install plugin → hooks activate automatically
  2. No configuration needed → works out of the box
  3. Merge with project hooks → runs alongside local hooks
  4. Parallel execution → doesn't block other hooks

Validation Examples

Good Commit Messages

feat(auth): adds password reset flow
fix(api): prevents null pointer in handler
docs: updates API documentation

Bad Commit Messages (Blocked)

❌ "add feature"           → Use "adds feature"
❌ "Added cool stuff"      → No type/scope
❌ "feat: Add feature 🎉"  → No emoji
❌ "Very long subject..."  → >72 chars blocked

Installation

claude plugin install dev-guard@personal-claude-marketplace

Requirements

  • Bash shell
  • Git repository

Built-in Rules

The guard includes approximately 90 built-in rules across several categories:

  • Command rules (~33): Native tool redirections, Python tooling, git safety, interactive commands, project conventions
  • URL rules (~26): GitHub, GitLab, Google, Atlassian, Slack authenticated URLs, plus bot-blocked and login-gated public sites (Reddit, Wikipedia, npm, Amazon, LinkedIn, and more) redirected to fetchaller-mcp — see references/fetchaller-domain-rules.md
  • Git deny rules (19): Reset --hard, force push, branch deletion, filter-branch, unsafe operations (always enforced)
  • Git ask rules (8): Stash drop, filter-repo, reflog delete/expire, remote removal, config modifications (can be trusted)
  • oc/kubectl introspection (~4): Critical, high, medium, low risk assessments (dynamic)

All rule names and guidance messages are defined in the source file dev-guard/hooks/tool-selection-guard.py.

Customization

Hooks use plugin-relative paths (${CLAUDE_PLUGIN_ROOT}) and work in any project without modification.

Both URL and command guard rules can be extended with user-defined rules via environment variables pointing to JSON files. Rules are loaded at startup, merged with built-ins, and checked by the same pipeline (chains, pipes, subshells, env prefix stripping). Bad config files are silently ignored.

Custom URL Guard Rules

Block additional URLs beyond the built-in authenticated-service and bot-blocked-site rules.

Set URL_GUARD_EXTRA_RULES to a JSON file path:

export URL_GUARD_EXTRA_RULES="$HOME/.config/claude/url-guard-rules.json"

JSON format — array of rule objects with name, pattern (regex matched against the full URL), message, and optional action ("block" or "ask", defaults to "block"):

[
    {
        "name": "internal-gitlab",
        "pattern": "gitlab\\.internal\\.example\\.com",
        "message": "This URL is on internal GitLab. Use glab CLI instead."
    },
    {
        "name": "staging-api",
        "pattern": "staging\\.api\\.example\\.com",
        "message": "Staging API — confirm this is intentional.",
        "action": "ask"
    }
]

Custom Command Guard Rules

Block specific command patterns while keeping the base command fully allowlisted in Claude's permissions. This is useful when you want Claude to use tools like oc, gh, or kubectl freely but need to prevent destructive subcommands.

Set COMMAND_GUARD_EXTRA_RULES to a JSON file path:

export COMMAND_GUARD_EXTRA_RULES="$HOME/.config/claude/command-guard-rules.json"

JSON format — array of rule objects with name, pattern (regex matched against the command), message, optional exception (regex — if the command also matches this, it is allowed), and optional action ("block" or "ask", defaults to "block"):

[
    {
        "name": "oc-delete",
        "pattern": "^\\s*oc\\s+delete\\b",
        "message": "oc delete is blocked. Use the OpenShift console for deletions.",
        "exception": "--dry-run"
    },
    {
        "name": "oc-scale-zero",
        "pattern": "^\\s*oc\\s+scale\\b.*--replicas=0",
        "message": "Scaling to zero — confirm this is intentional.",
        "action": "ask"
    },
    {
        "name": "gh-repo-delete",
        "pattern": "^\\s*gh\\s+repo\\s+delete\\b",
        "message": "gh repo delete is blocked. Delete repositories via the GitHub UI."
    }
]

Custom command rules inherit all built-in processing:

  • Checked across chained commands (&&, ||, ;)
  • Checked in pipe segments and subshells ($(), backticks)
  • Environment variable prefixes are stripped before matching (KUBECONFIG=x oc delete matches oc delete)
  • GUARD_BYPASS=1 prefix overrides tool/command rules (git safety rules still enforced)

Pipe Safety and Segment Checking

Pipes (|) are analyzed segment-by-segment to prevent dangerous patterns while allowing legitimate filtering:

  • Pipe-aware matching: Commands connected by | are checked individually. The first segment is checked normally by all rules; subsequent segments are checked with limited context since they're processing piped output rather than reading files.
  • Skipped rules in pipes: Certain rules like cat-file and echo-noop are skipped in pipe context because after | they're legitimate filtering operations. For example, some-command | cat doesn't trigger the cat redirect rule since it's filtering output, not doing the file operation that native tools would replace. Exception: echo-noop and printf-noop are enforced on the terminal (last) pipe segment, since that output goes to the user rather than feeding another command downstream.
  • Allow rules and pipes: Even if the first segment matches an action: "allow" rule and would exit 0, dangerous subsequent pipe segments are still checked and can block or ask. Example: safe-command | git reset --hard is blocked on the git reset rule regardless of the first segment's allow status.
  • Processing order: For each subcommand, pipes and subshells are checked before the full command rules. This ensures dangerous segments trigger deny/ask before any allow rule can short-circuit.

Action Field

Both URL and command rules support an optional action field:

ValueMechanismBehaviorDefault
"block"Exit 2 + stderrHard deny — Claude cannot proceedYes
"ask"JSON hookSpecificOutput + exit 0Prompts the user for confirmationNo
"allow"JSON hookSpecificOutput + exit 0Explicitly allows the command via the hook, bypassing Claude's permission system. Exit code 0 with permissionDecision: "allow" JSON.No

"block" rules always take precedence (exiting 2 immediately across all subcommands, pipes, subshells, and URLs). Among non-block matches, untrusted "ask" rules take precedence over "allow" and trusted "ask" rules, and "allow" is only emitted on compound commands when every subcommand is covered.

The "ask" action outputs permissionDecision: "ask" via Claude Code's PreToolUse hook JSON protocol. Claude Code displays a permission prompt with the rule's message as the reason. This overrides permissions.allow auto-approve rules — even commands matching Bash(git:*) or Bash(oc:*) will prompt when a hook returns "ask".

The "allow" action explicitly permits the command via permissionDecision: "allow" in the JSON response, bypassing Claude's default permission system. This is useful for commands that would normally be blocked but are safe in specific contexts.

⚠️ Important: "ask" and "allow" require the JSON hookSpecificOutput protocol

Both actions use permissionDecision: "ask" or permissionDecision: "allow" in a JSON object on stdout with exit code 0. They do not use sys.exit(1) — exit code 1 is a non-blocking error in Claude Code and the command will proceed silently. If you're writing custom hooks, use the JSON protocol:

{"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "ask", "permissionDecisionReason": "reason"}}

Known issue (VS Code): The VS Code extension ignores permissionDecision: "ask" and permissionDecision: "allow" and falls back to permission rules (#13339). The CLI works correctly. Use "block" as a workaround if VS Code support is needed.

Config Validation

Run --validate to check your config files for issues:

uv run dev-guard/hooks/tool-selection-guard.py --validate

This checks both URL_GUARD_EXTRA_RULES and COMMAND_GUARD_EXTRA_RULES files for:

  • Missing files, invalid JSON, wrong types
  • Missing required fields (name, pattern, message)
  • Invalid regex patterns and exceptions
  • Empty patterns (matches everything) or empty exceptions (disables rule)
  • Invalid action values

Validation runs automatically on session start via the SessionStart hook. If your config has issues, you'll see them immediately when a new session begins. Built-in rules are always enforced regardless of config file state.

Unified Audit Log

All hook decisions (blocked, asked, allowed, trusted) are logged to a SQLite database at ~/.claude/logs/dev-guard.db with Write-Ahead Logging (WAL) for concurrent multi-session access.

Database location: ~/.claude/logs/dev-guard.db (override with GUARD_DB_PATH environment variable)

Logging verbosity is controlled via the GUARD_LOG_LEVEL environment variable:

  • off — No logging
  • actions — Log only blocked, asked, trusted, and allowed decisions (default)
  • all — Log all hook invocations including skipped rules and non-matching patterns

Schema overview: The audit log stores category, action, rule name, matched command, and timestamp for each entry.

Credential redaction: Commands logged to the audit database are automatically scrubbed of potential secrets (tokens, passwords, API keys, bearer auth) before storage. Patterns like --token XXXX or password=XXXX are replaced with [REDACTED].

Security note: The audit log database file is created with 0o600 permissions (owner read/write only) because it may contain sensitive command-line data, including file paths or other operational details.

Trust Management

Users can trust ask rules to auto-approve them without manual confirmation:

/dev-guard:trust add <rule-name> --scope session --session-id <id>                    # Add to session trust
/dev-guard:trust add <rule-name> --scope always                                       # Add to persistent trust (all sessions)
/dev-guard:trust add <rule-name> --match <pattern> --scope session --session-id <id>  # Trust only commands matching substring
/dev-guard:trust remove <rule-name>                                                   # Remove from trust
/dev-guard:trust list                                                                 # Show trusted rules

Important:

  • Block rules are never trustable (only ask rules can be trusted)
  • Trust entries are stored in SQLite alongside the audit log
  • Session-scoped trust requires an explicit --session-id <id> (included in the trust hint on every ask prompt)
  • Users receive a trust hint when prompted: "To trust this rule, run: /dev-guard:trust add <rule> --scope always"

Workflow example:

  1. Run a command that triggers an ask rule
  2. Receive permission prompt with trust hint
  3. Approve the command
  4. Run /dev-guard:trust add remote-remove --scope always to auto-approve in the future
  5. Next session: same command auto-approves without prompting

Processing Pipeline

The guard processes each command through a structured pipeline:

stdin JSON → parse → session state → hook dispatch (PreToolUse or PostToolUse) →
  tmp-path guards → plan-mode redirect →
  WebFetch URL check → Bash command analysis →
    split_commands (&&, ||, ;, newline) →
    per-subcmd: pipes → subshells → command rules → oc introspection →
  passthrough exit(0)

Key stages:

  1. JSON parse: Extracts tool_name, tool_input, session_id from stdin
  2. Session state: Persists session ID to database for trust scope validation
  3. Early exits: Non-Bash tools pass through; EnterPlanMode redirects to skill. tmp-path guards inspect Edit/Write/NotebookEdit paths, and the comment-narration guard inspects their content (skipping Markdown/text/doc files), before dispatch.
  4. GUARD_BYPASS check: If prefix present, enforce only GIT_DENY_RULES
  5. Fetch command check: curl/wget commands checked against BLOCKED_URL_RULES
  6. Split commands: Break on command delimiters (&&, ||, ;, newline)
  7. Per-subcommand analysis:
    • Extract bash -c inner commands
    • Check pipe segments (skip native-tool rules)
    • Check subshells recursively
    • Check full subcommand against all rules
    • Introspect oc/kubectl commands for resource risk
    • Validate kill/killall/pkill targets against Claude session process tree
  8. Logging: Audit events recorded to SQLite with category, action, rule, command

RTK Compression

Automatically compresses CLI output using rtk for commands that pass all guard rules. Deny rules fire first — blocked commands never reach RTK.

Prerequisites: brew install fredrikaverpil/tap/rtk. Graceful degradation if absent.

How it works:

  1. All deny rules, git safety, and convention checks run first
  2. Commands that pass all checks are sent to rtk rewrite for rewrite detection
  3. If rtk supports the command, updatedInput rewrites it (e.g., git log → rtk git log)
  4. If rtk doesn't support it, the command passes through unchanged
  5. Only single, non-piped, non-subshell, non-multiline commands are eligible

Supported commands: RTK supports 40+ command families including git, cargo, docker, kubectl, pytest, go, ruff, curl, ls, grep, and more. See rtk --help for the full list. Dev-guard delegates to rtk rewrite which is the single source of truth.

Config: On session start, dev-guard creates/patches rtk's config.toml (path from rtk config):

  • [telemetry] enabled = false — telemetry disabled
  • [tee] mode = "always" — full uncompressed output always saved to rtk's tee directory (~/Library/Application Support/rtk/tee/ on macOS, ~/.local/share/rtk/tee/ on Linux)

Full output access: When rtk compresses output, agents can read the uncompressed version from the tee directory. PostToolUse hooks on Read and Bash track when tee files are accessed (logged to rtk_events table).

Analytics: RTK compression events are logged to rtk_events in ~/.claude/logs/dev-guard.db:

SELECT event_type, COUNT(*) FROM rtk_events GROUP BY event_type;
SELECT command, COUNT(*) as n FROM rtk_events
  WHERE event_type = 'compressed' GROUP BY command ORDER BY n DESC;

Disabling: Set RTK_DISABLED=1 environment variable to skip all RTK rewrites.

oc/kubectl Introspection

Mutating oc and kubectl commands receive automatic risk assessment based on the resource types being modified, with enhanced prompts showing detected security risks.

Risk tiers and resource types:

Risk LevelResource Types
Criticalsecret, clusterrole, clusterrolebinding, webhookconfigurations
Highnamespace, project, node, persistentvolume, CRD, serviceaccount, networkpolicy
Mediumdeployment, statefulset, daemonset, service, ingress, route, configmap, role, rolebinding, cronjob, job
Lowpod, event, resourcequota, limitrange, HPA, PDB

YAML manifest inspection:

  • Parses YAML files passed via stdin or files to detect resource kinds and security fields
  • Uses regex-based parsing (no external dependencies)
  • Detects security risk fields: privileged, hostNetwork, hostPID, hostPath, runAsRoot
  • Handles pipe patterns: cat file.yaml | oc apply -f -

Automatic allowances:

  • Commands with --dry-run are always allowed without prompting (safe preview mode)
  • User-defined command rules take priority over built-in introspection

Kill Command Guard

Validates kill, killall, and pkill commands against the Claude Code session process tree. Commands targeting Claude-spawned child processes are allowed silently. Commands targeting external processes or unresolvable targets trigger an ask prompt with process context.

Covered commands:

  • kill <PID> — Single PID termination
  • killall <name> — Kill by process name
  • pkill <pattern> — Kill by pattern match

Behavior matrix:

ScenarioBehavior
PID is Claude-spawned childAllow silently
PID is external (not Claude child)Ask prompt with context
Job reference (kill %1)Allow silently (always Claude-spawned)
Informational flags (kill -l, kill -L)Allow silently
xargs kill or pipe-to-killAsk prompt (can't verify statically)
50+ resolved PIDsAsk prompt (DoS protection)
Mixed ownership (some Claude, some external)Ask prompt showing both lists

Process tree validation:

  • Uses ps to walk the OS process tree — no state tracking needed
  • Resolves job references and named patterns to PIDs via pgrep
  • Shows Claude vs. external process lists in ask prompts

Custom rule interaction:

  • Custom command rules take priority (checked earlier in _check_rules())
  • Users can trust the ask rule for auto-approval

Trustable Rule Names

Users can trust these built-in ask-type rules with /dev-guard:trust add <rule-name>:

Git safety rules (ask actions):

  • stash-drop — Destructive stash operations
  • config-global-write — Global git config modifications
  • filter-repo — History-rewriting git filter-repo
  • reflog-delete-expire — Reflog delete/expire operations
  • remote-remove — Removing a git remote
  • branch-from-local-main — Branching from local main (may be stale)
  • branch-from-non-upstream — Branching from non-upstream refs (stacking risk)
  • branch-needs-fetch — Requires fetch before rebase (dynamic, matches current branch)
  • skip-env-bypass — SKIP=/PREK_SKIP= selectively bypassing pre-commit/prek hooks

oc/kubectl introspection rules (ask actions):

  • oc-critical — Critical resource modifications
  • oc-high — High-risk resource modifications
  • oc-medium — Medium-risk resource modifications

Kill command guard rules (ask actions):

  • kill-non-claude-process — Killing processes not spawned by this Claude session

MCP guard rules (ask actions):

  • fetchaller-fetch-mutating-call — Mutating (POST) or custom-header/body calls to fetchaller-mcp's fetch tool

Bypass Mechanisms

PrefixScopeUse case
GUARD_BYPASS=1All Bash rules except GIT_DENY_RULESOverride tool selection and command guard rules; GIT_DENY_RULES (reset --hard, force push, branch -D, etc.) is always enforced — GIT_ASK_RULES (e.g. stash-drop) is bypassed like everything else
ALLOW_FETCH=1URL rules (curl/wget only)Fetch an authenticated URL after confirming alternatives

OMP Compatibility

Also ships an omp-extension.ts bridge (package.json declares it via omp.extensions) so this plugin's hooks work under OMP too, by shelling out to the same Python/shell scripts documented above, unchanged — no guard logic is reimplemented in TypeScript. See OMP-COMPAT.md for the full tool-name/field-name mapping table, MCP re-encoding details, and event mapping.

Known limitation: SubagentStop has no confirmed OMP event of its own — it's dispatched as a best-effort approximation on every agent_end firing (the same event used for Stop), since no OMP event distinguishes a subagent's completion from the main session's. This is a quality/completeness control, not a security gate, so the approximation degrades usefulness in the worst case, not safety.

Author

wgordon17 - January 2026