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)
| Command | Description |
|---|---|
/dev-guard:guard-stats | Query dev-guard audit database for trigger trends, trust candidates, and noise signals |
/dev-guard:trust | Manage 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/sedto Grep/Glob/Read/Edit tools - Python tooling — Enforces
uv run/uvxover barepython/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
EnterPlanModeto 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
AskUserQuestioncalls tagged with▸dp:metadata and auto-answers viaupdatedInputwhen 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
updatedInputrequirespermissionDecisionto 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/v2framing, "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, anduser_deferredarrays 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:
- Install plugin → hooks activate automatically
- No configuration needed → works out of the box
- Merge with project hooks → runs alongside local hooks
- 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 deletematchesoc delete) GUARD_BYPASS=1prefix 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-fileandecho-noopare skipped in pipe context because after|they're legitimate filtering operations. For example,some-command | catdoesn't trigger the cat redirect rule since it's filtering output, not doing the file operation that native tools would replace. Exception:echo-noopandprintf-noopare 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 --hardis 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:
| Value | Mechanism | Behavior | Default |
|---|---|---|---|
"block" | Exit 2 + stderr | Hard deny — Claude cannot proceed | Yes |
"ask" | JSON hookSpecificOutput + exit 0 | Prompts the user for confirmation | No |
"allow" | JSON hookSpecificOutput + exit 0 | Explicitly 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 JSONhookSpecificOutputprotocolBoth actions use
permissionDecision: "ask"orpermissionDecision: "allow"in a JSON object on stdout with exit code 0. They do not usesys.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"andpermissionDecision: "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
actionvalues
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 loggingactions— 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 everyaskprompt) - Users receive a trust hint when prompted: "To trust this rule, run:
/dev-guard:trust add <rule> --scope always"
Workflow example:
- Run a command that triggers an ask rule
- Receive permission prompt with trust hint
- Approve the command
- Run
/dev-guard:trust add remote-remove --scope alwaysto auto-approve in the future - 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:
- JSON parse: Extracts tool_name, tool_input, session_id from stdin
- Session state: Persists session ID to database for trust scope validation
- 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.
- GUARD_BYPASS check: If prefix present, enforce only GIT_DENY_RULES
- Fetch command check: curl/wget commands checked against BLOCKED_URL_RULES
- Split commands: Break on command delimiters (&&, ||, ;, newline)
- 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
- 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:
- All deny rules, git safety, and convention checks run first
- Commands that pass all checks are sent to
rtk rewritefor rewrite detection - If rtk supports the command,
updatedInputrewrites it (e.g.,git log→rtk git log) - If rtk doesn't support it, the command passes through unchanged
- 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 Level | Resource Types |
|---|---|
| Critical | secret, clusterrole, clusterrolebinding, webhookconfigurations |
| High | namespace, project, node, persistentvolume, CRD, serviceaccount, networkpolicy |
| Medium | deployment, statefulset, daemonset, service, ingress, route, configmap, role, rolebinding, cronjob, job |
| Low | pod, 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-runare 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 terminationkillall <name>— Kill by process namepkill <pattern>— Kill by pattern match
Behavior matrix:
| Scenario | Behavior |
|---|---|
| PID is Claude-spawned child | Allow 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-kill | Ask prompt (can't verify statically) |
| 50+ resolved PIDs | Ask prompt (DoS protection) |
| Mixed ownership (some Claude, some external) | Ask prompt showing both lists |
Process tree validation:
- Uses
psto 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 operationsconfig-global-write— Global git config modificationsfilter-repo— History-rewriting git filter-reporeflog-delete-expire— Reflog delete/expire operationsremote-remove— Removing a git remotebranch-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 modificationsoc-high— High-risk resource modificationsoc-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 tofetchaller-mcp'sfetchtool
Bypass Mechanisms
| Prefix | Scope | Use case |
|---|---|---|
GUARD_BYPASS=1 | All Bash rules except GIT_DENY_RULES | Override 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=1 | URL 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