Why runok?
Even with allow rules configured, Claude Code asks for confirmation in cases like these:
# Claude adds a comment before the command -- no longer matches your allow rule
)
# Text-based parsing flags safe commands as suspicious
&& && )
runok parses commands with tree-sitter-bash, so comments, compound commands (&&, |, ;), and wrapper commands (sudo, bash -c, xargs) are all handled correctly. Each sub-command is evaluated independently against your rules.
And that's just the start. Claude Code's built-in permissions have other limitations too:
Denied commands give no explanation. The agent has no idea why a command was blocked. With runok, deny rules include a message and a suggested fix -- the agent reads it and self-corrects:
# runok.yml
- deny: 'git push -f|--force *'
message: 'Force push is not allowed.'
fix_suggestion: 'git push --force-with-lease'
Global flags break matching. Claude sometimes adds flags like -C before the subcommand. git -C /path commit does not match Bash(git commit *). runok handles this with optional groups and order-independent matching:
# runok.yml
- allow: 'git [-C *] commit *'
# matches: git commit -m "fix"
# matches: git -C /path/to/repo commit -m "fix"
No recursive parsing of wrappers. Claude Code does not inspect sudo, bash -c, or $(). runok recursively unwraps them to evaluate the inner command:
# runok.yml
definitions:
wrappers:
- 'sudo <cmd>'
- 'bash -c <cmd>'
rules:
- deny: 'rm -rf /'
# "sudo bash -c 'rm -rf /'" -> unwrap sudo -> unwrap bash -c -> deny
JSON only, no comments. settings.json cannot be annotated. runok uses YAML:
# runok.yml
rules:
# read-only git commands are always safe
- allow: 'git status'
- allow: 'git diff *'
# allow push, but not force push -- rewrites shared history
- deny: 'git push -f|--force *'
message: 'Use --force-with-lease instead.'
- ask: 'git push *'
See Why runok? for a full comparison table.
Features
Flexible command parsing
tree-sitter-bashAST parsing -- comments, pipes,&&,;are understood, not treated as opaque stringssudo,bash -c,xargsare recursively unwrapped so rules apply to the inner command
Flexible rule configuration
- Wildcards, flag alternation (
-f|--force), optional groups, argument-order-independent matching - Conditional
whenclauses with CEL expressions for environment-aware decisions - OS-level sandboxing (macOS Seatbelt / Linux Landlock) for file and network restrictions
And more -- preset sharing, denial feedback, extension protocol, Claude Code plugin for natural-language configuration assistance
Quick start
Install
Homebrew
From source with Cargo
Pre-built binaries are also available on GitHub Releases. See Installation for all options.
Configure
The fastest way to get started is with the interactive setup wizard:
This creates a runok.yml, and if you have Claude Code configured, migrates your Bash permissions to runok rules and registers the PreToolUse hook automatically.
You can also configure manually. Create ~/.config/runok/runok.yml:
rules:
- allow: 'git status'
- allow: 'git diff *'
- allow: 'git log *'
- ask: 'git push *'
- deny: 'git push -f|--force *'
message: 'Force push is not allowed'
defaults:
action: ask
And add runok as a PreToolUse hook in .claude/settings.json:
See Claude Code Integration for sandbox setup and advanced configuration.
Verify
Full Documentation
See runok.fohte.net
Feedback
Feature requests and bug reports are welcome on GitHub Issues.