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:

  1. Validator returns a blocking error with an error code (e.g., GIT019)
  2. klaudiush scans the command for an exception token - env var first, then shell comment
  3. Token error code must exactly match the blocking error code
  4. Policy check: allow_exception must be true, reason rules must pass
  5. Rate limit check: global and per-code limits both must pass
  6. If all checks pass: block becomes a warning tagged [BYPASSED], audit entry written
  7. 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:reason does not match because there is no whitespace before EXC:.
  • 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"
OptionDefaultDescription
allow_exceptiontrueAllow exceptions for this code
require_reasonfalseRequire justification
min_reason_length10Minimum reason length in runes, not bytes
valid_reasons[]Pre-approved reasons (exact match, case-insensitive)
max_per_hour0Hourly limit (0 = unlimited)
max_per_day0Daily 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_name
  • allowed (bool), reason, denial_reason
  • source - "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"

© 2026 Smykla Skalski Labs