Skip to content

fohte/runok

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

189 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

runok

CI GitHub Release License: MIT

runok is a command permission tool for LLM coding agents.

📖 Documentation · 🚀 Quick Start · 🔧 Configuration · 📋 Recipes


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
⏺ Bash(# check logs
      git log --oneline -5)
  ⎿  Running…

Command contains newlines that could separate multiple commands

Do you want to proceed?
❯ 1. Yes
  2. No
# Text-based parsing flags safe commands as suspicious
⏺ Bash(git log --oneline -5 && echo "---" && git status)
  ⎿  Running…

Command contains quoted characters in flag names

Do you want to proceed?
❯ 1. Yes
  2. No

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-bash AST parsing -- comments, pipes, &&, ; are understood, not treated as opaque strings
  • sudo, bash -c, xargs are recursively unwrapped so rules apply to the inner command

Flexible rule configuration

  • Wildcards, flag alternation (-f|--force), optional groups, argument-order-independent matching
  • Conditional when clauses with CEL expressions for environment-aware decisions
  • OS-level sandboxing (macOS Seatbelt / Linux Landlock) for file and network restrictions

And more -- rule testing, audit logging, preset sharing, denial feedback, extension protocol, Claude Code plugin for natural-language configuration assistance

Quick start

Install

Homebrew

brew install fohte/tap/runok

From source with Cargo

cargo install --git https://github.com/fohte/runok.git

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:

runok init

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:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "runok check --input-format claude-code-hook"
          }
        ]
      }
    ]
  }
}

See Claude Code Integration for sandbox setup and advanced configuration.

Verify

runok check -- git status        # => allow
runok check -- git push -f main  # => deny
runok check -- git push main     # => ask

Full Documentation

See runok.fohte.net

Feedback

Feature requests and bug reports are welcome on GitHub Issues.

About

Command execution permission framework for LLM agents

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages