On this page
How claudewheel guardrails work: the four enforcement tiers, subagent-versus-main-agent handling, command-string caveats, and upgrading existing profiles.
#Guardrails
claudewheel ships a canonical set of command guardrails that every profile inherits. The guardrails discourage or block destructive shell commands (bulk git add, rm, history rewrites, branch deletion) and steer agents toward safe alternatives such as safegit and saferm. The rules live in one place and drive both the deployed hook scripts and each profile's permission arrays.
#Enforcement tiers
Every rule belongs to exactly one of four tiers. The tier decides where the rule is enforced (a PreToolUse/PostToolUse hook, the settings deny/ask arrays, or both) and who it applies to. The hook is always the authoritative enforcer when a rule has one; the settings arrays are best-effort defense-in-depth for the plain command form.
- HARD_DENY -- denied for everyone, both the main agent and subagents, via
a PreToolUse hook. A backing settings deny glob may exist as defense-in-depth, but it never reproduces the hook's full match surface (compound commands, sudo/env/xargs/find -exec wrappers, alternate remotes). Some HARD_DENY rules own no deny glob at all.
- ESCALATE -- denied only when a subagent attempts the command. The main
agent falls through the hook silently so the settings ask rule prompts the user to approve it deliberately.
- ADVISE -- the command runs, then a
PostToolUsehook nudges the agent
with advice via additionalContext. There are no settings entries and nothing is blocked.
- ASK -- a pure settings
askrule with no hook involvement. The user is
prompted before the command runs.
#Subagents versus the main agent
The blocker hook distinguishes a subagent from the main agent using the agent_id field in the PreToolUse payload. Claude Code populates agent_id only for subagent tool calls, so a non-empty agent_id marks a subagent. HARD_DENY rules block regardless of agent_id, while ESCALATE rules block only when agent_id is set and otherwise let the main agent through.
This is why an ESCALATE command like git push is refused outright for a subagent (with a message telling it to report to its parent) but merely prompts the user when the main agent runs it. The distinction keeps risky, outward-facing actions in the hands of the human-supervised main agent.
#Command-string caveat
The hooks match against the raw command string with grep -qE, anchored to the start of a shell segment. They do not parse the shell. A command that only mentions a guarded token -- for example inside an echo, a grep pattern, a comment, or a heredoc -- can still trip the matcher and be nudged or blocked even though nothing dangerous would actually run.
This is a deliberate trade-off: false positives are safe (you rephrase or split the command), whereas parsing the shell to eliminate them would be far more fragile than a conservative string match. When a benign command is blocked, move the guarded token out of the command line or run the pieces separately.
#Upgrading existing profiles
The guardrail model evolves between releases. Existing profiles keep whatever rules were current when they were created, so after upgrading claudewheel you should re-apply the canonical model to bring older profiles up to date:
- Run
claudewheel reconcile-permissions --applyto rewrite each profile's
deny/ask/allow permission arrays to match the current model.
- Run
claudewheel patch-profilesto sync the deployed hook scripts and
disallowedTools defaults into every profile and shared-settings.json.
Both commands support --dry-run so you can preview the changes before writing anything to disk.
#Rule reference
The table below is generated directly from the canonical rule set, so it always reflects the guardrails shipped in this version. "Settings coverage" reports how completely a rule's deny/ask glob(s) track its hook surface (FULL, PARTIAL, or NONE), or n/a for tiers with no settings backstop.
| Key | Tier | Settings coverage | Advice |
|---|---|---|---|
rm | HARD_DENY | PARTIAL | Use 'saferm delete --description "why" file1 file2' instead of 'rm'. |
git-add-bulk | HARD_DENY | PARTIAL | Use 'safegit commit -m "msg" -- file1 file2' instead of 'git add'. |
git-stash | HARD_DENY | FULL | Use 'safegit commit' on a temporary branch instead of 'git stash'. |
git-restore | HARD_DENY | FULL | Use the Edit tool to revert specific lines instead of 'git restore'. |
git-checkout-file | HARD_DENY | NONE | Use the Edit tool to revert specific lines instead of 'git checkout -- file'. |
git-checkout | HARD_DENY | FULL | 'git checkout' is deprecated here; use 'git switch' for branches (plain git switch is allowed) or the Edit tool to revert files. |
git-push-delete | HARD_DENY | PARTIAL | Deleting remote branches is destructive; ask the user to do this deliberately. |
push | ESCALATE | FULL | Pushes happen only via rlsbl (rlsbl release run / rlsbl push). |
git-reset | ESCALATE | PARTIAL | git reset is destructive in shared worktrees. |
git-switch-force | ESCALATE | FULL | Forced switch destroys uncommitted work in shared worktrees. |
gh-workflow-run | ESCALATE | FULL | Triggering CI workflows is an outward-facing action. |
saferm-purge | ESCALATE | FULL | saferm purge permanently destroys archived files. |
git-rebase | ESCALATE | PARTIAL | Rebase rewrites history in shared worktrees. |
safegit-rewrite-author | ESCALATE | FULL | Author rewriting is history rewriting. |
kill | ADVISE | n/a | This kill/pkill ran, but prefer building graceful stop commands or PID-file-based stop scripts into your tooling instead of killing processes directly. |
sudo | ASK | n/a | Prompted via the settings ask rule (no hook). |