Exception workflow
Bypass validation with audit trail
On this page
Overview
The exception workflow lets Claude Code bypass specific validation blocks with an explicit
acknowledgment token. When klaudiush blocks a command, adding an EXC: token
to the command overrides the block. Every attempt, allowed or denied, is logged to disk
before the result is returned. Rate limits and per-code policies apply.
By default, any error code can be bypassed if a valid token is provided. Set require_explicit_policy = true to restrict bypasses to codes that have an
explicit policy entry.
Quick start
Enable exceptions and define a policy for the error code you want to bypass:
[exceptions]
enabled = true
[exceptions.policies.GIT019]
enabled = true
allow_exception = true
require_reason = true
min_reason_length = 10
description = "Exception for pushing to protected branches" When Claude encounters a block, it can add a token to bypass it:
# Shell comment format
git push origin main # EXC:GIT019:Emergency+hotfix+for+production
# Environment variable format
KLACK="EXC:GIT019:Emergency+hotfix" git push origin main How it works
When a validator returns a blocking error, the dispatcher runs the exception check before returning the result to Claude Code:
- Validator returns a blocking error with an error code (e.g.,
GIT019) - klaudiush scans the command for an exception token - env var first, then shell comment
- Token error code must exactly match the blocking error code
- Policy check:
allow_exceptionmust be true, reason rules must pass - Rate limit check: global and per-code limits both must pass
- If all checks pass: block becomes a warning tagged
[BYPASSED], audit entry written - If any check fails: original block stands, audit entry written with denial reason
A successfully bypassed block becomes a non-blocking warning, so Claude Code can continue. A failed bypass or missing token leaves the original block in place.
Token format
Exception tokens follow this format: EXC:<ERROR_CODE>:<URL_ENCODED_REASON>
Placement
Tokens can go in a shell comment (recommended) or the KLACK environment
variable. When both are present, the env var takes priority.
# Shell comment (recommended)
git push origin main # EXC:GIT019:Emergency+hotfix
# Environment variable
KLACK="EXC:SEC001:Test+fixture" git commit -sS -m "Add test data" URL encoding
Reasons must be URL-encoded: spaces become +, # becomes %23, etc. This avoids shell parsing issues.
Parsing rules
Two constraints prevent accidental or injected matches:
# Word boundary required - token must start after whitespace
git push origin main # EXC:GIT019:reason <- matches
git push origin main # NOEXC:GIT019:reason <- no match
# Variable expansion is rejected - only literal strings work
KLACK="EXC:${CODE}:reason" git push # <- token not found
KLACK="EXC:$(echo GIT019):reason" git push # <- token not found
# When both are present, env var wins
KLACK="EXC:GIT019:Env+reason" git push # EXC:GIT019:Comment+reason - The token must start at a word boundary (after whitespace or at the start of a
comment).
NOEXC:GIT019:reasondoes not match because there is no whitespace beforeEXC:. - Only literal strings are accepted. Tokens containing
${...}or$(...)are rejected entirely.
Policy configuration
Each error code gets its own policy with independent settings:
[exceptions.policies.GIT019]
enabled = true
allow_exception = true
require_reason = true
min_reason_length = 15
valid_reasons = ["emergency hotfix", "approved by lead", "security patch"]
max_per_hour = 5
max_per_day = 20
description = "Exception for pushing to protected branches" | Option | Default | Description |
|---|---|---|
allow_exception | true | Allow exceptions for this code |
require_reason | false | Require justification |
min_reason_length | 10 | Minimum reason length in runes, not bytes |
valid_reasons | [] | Pre-approved reasons (exact match, case-insensitive) |
max_per_hour | 0 | Hourly limit (0 = unlimited) |
max_per_day | 0 | Daily limit (0 = unlimited) |
Requiring explicit policies
By default, any error code can be bypassed as long as a valid token is provided. Set require_explicit_policy = true in the [exceptions] block to
restrict bypasses to codes that have an explicit policy entry. Tokens for unconfigured
codes are denied.
# Without this, any error code can be bypassed by default
[exceptions]
require_explicit_policy = true
# Only codes with an explicit policy entry can now be bypassed
[exceptions.policies.GIT019]
allow_exception = true
require_reason = true Reason matching
When valid_reasons is set, the decoded reason must exactly match one of the
listed values (case-insensitive). Prefix matching is not used - "Emergency hotfix
for prod" does not satisfy approved reason "Emergency hotfix".
Length is counted in runes, so CJK characters and emoji each count as one.
Rate limiting
Global and per-code rate limits cap how often exceptions can be used. Both must pass for a bypass to succeed. Global limits apply across all error codes combined; per-code limits are set in the policy entry.
[exceptions.rate_limit]
enabled = true
max_per_hour = 10 # global, all codes combined
max_per_day = 50
# Per-code limits are set in the policy entry
[exceptions.policies.GIT019]
max_per_hour = 2
max_per_day = 5 Hourly windows reset on the hour. Daily windows reset at local midnight, not UTC, so
limits turn over when the day changes in the user's timezone. Rate limit state is
per-project: each project gets its own counters at $XDG_DATA_HOME/klaudiush/exceptions/state_<hash>.json (default ~/.local/share/klaudiush/exceptions/), derived from the project
directory path. One project's exception usage does not affect another.
Audit logging
Every exception attempt - allowed or denied - is appended as a JSON line to $XDG_STATE_HOME/klaudiush/exception_audit.jsonl (default ~/.local/state/klaudiush/exception_audit.jsonl). The audit log is
global across all projects. Entries are fsynced to disk before returning, so no bypass goes unrecorded
even if the process exits immediately after.
Each entry includes:
timestamp,error_code,validator_nameallowed(bool),reason,denial_reasonsource-"comment"or"env_var"command(truncated to 200 chars),working_dir,repository
# List all entries
klaudiush audit list
# Filter by error code
klaudiush audit list --error-code GIT019
# Filter by outcome
klaudiush audit list --outcome allowed
# View statistics
klaudiush audit stats
# Remove old entries
klaudiush audit cleanup Audit log rotation is configured under [exceptions.audit]: max_size_mb, max_age_days, and max_backups control when and how old log
files are rotated. audit cleanup manually removes entries older than max_age_days.
Debug commands
Use debug exceptions to inspect the active policy configuration for the
current project. Add --state to also show current rate limit counters.
# Show exception policies for current project
klaudiush debug exceptions
# Include current rate limit counters
klaudiush debug exceptions --state Integration with rules
Custom rules can support exception bypasses by adding a reference to the block action.
Built-in validator error codes (GIT001-GIT024, FILE001-FILE005, SEC001-SEC005)
work the same way.
# Custom rule with exception support
[[rules.rules]]
name = "block-production-deploy"
priority = 100
[rules.rules.match]
command_pattern = "*kubectl apply*production*"
[rules.rules.action]
type = "block"
message = "Production deployments require approval"
reference = "DEPLOY001"
# Exception policy for that reference
[exceptions.policies.DEPLOY001]
enabled = true
require_reason = true
min_reason_length = 20
valid_reasons = ["approved by SRE", "emergency rollback"] Examples
Strict policy (no exceptions)
[exceptions.policies.SEC003]
enabled = true
allow_exception = false
description = "Never allow exceptions for private key commits" Test fixture secrets
[exceptions.policies.SEC001]
enabled = true
require_reason = true
valid_reasons = ["test fixture", "mock data", "example config"]
description = "Allow secrets in test files"