Skip to content

Architecture Overview

This page describes the internal architecture of runok for contributors and advanced users who want to understand how commands are evaluated and executed.

When runok receives a command, it flows through the following stages:

StepStageDescription
1Config Loading4-layer merge + preset resolution
2Command ParsingTokenization + compound command splitting
3Rule EvaluationPattern matching + when-clause evaluation
4Action DecisionAllow / Deny / Ask / Default
5Sandbox ResolutionPreset lookup + strictest-merge
6Command ExecutionSandboxed or direct execution

runok merges configuration from four layers in ascending priority (global config, global local override, project config, project local override).

The extends field triggers recursive preset resolution (DFS with cycle detection, max depth 10). Presets can be loaded from local paths or remote GitHub repositories.

Source: src/config/loader.rs, src/config/preset.rs

The command parser (src/rules/command_parser.rs) handles two tasks:

  • Tokenization: Shell-aware splitting that respects single/double quotes and backslash escapes.
  • Compound command splitting: Uses tree-sitter-bash to decompose pipelines (|), logical operators (&&, ||), semicolons (;), subshells, loops, conditionals, and command substitutions into individual commands.

Each individual command is then structurally parsed using a FlagSchema inferred from rule patterns (see Pattern Matching).

The rule engine (src/rules/rule_engine.rs) evaluates each command against the configured rules:

  • Single commands: Each rule’s pattern is tested against the command via the pattern matching pipeline, then any when clauses are evaluated using a CEL expression evaluator.
  • Compound commands: Each sub-command is evaluated individually, then results are aggregated using the Explicit Deny Wins principle.
  • Wrapper commands: If a command matches a wrapper definition (e.g., bash -c <cmd>, sudo <cmd>), the inner command captured by the <cmd> placeholder is recursively evaluated (max depth 10).

The rule engine returns one of four actions:

ActionMeaning
AllowCommand is permitted to execute
DenyCommand is rejected (with optional reason and suggestion)
AskUser confirmation is required
DefaultNo rule matched; falls back to configured default behavior

For compound commands, actions are aggregated by priority: Deny > Ask > Allow > Default.

If a matching rule specifies a sandbox preset name, the adapter resolves it to a concrete policy:

  1. Look up the preset in definitions.sandbox
  2. Resolve CWD-relative paths to absolute paths
  3. For compound commands, merge all sub-command policies using a strictest-wins strategy:
    • writable paths: intersection (more restrictive)
    • deny paths: union (all denied paths combined)
    • network: AND (denied if either denies)

If no rule-level sandbox is specified, the global defaults.sandbox is applied as a fallback.

The executor layer (src/exec/) runs the command in one of three modes:

ModeDescription
TransparentProxyReplaces the current process via exec syscall
SpawnAndWaitSpawns a child process and waits for it to complete
ShellExecRuns through sh -c for shell features

When a sandbox policy is active, a platform-specific sandbox wraps the execution:

  • macOS: Generates an SBPL (Seatbelt Profile Language) profile and runs the command through sandbox-exec.
  • Linux: Uses Landlock LSM for filesystem access control.

The source code (src/) is organized into four top-level modules:

ModuleResponsibility
cli/CLI argument definitions and subcommand routing
config/Config data model, 4-layer loading/merging, and preset resolution
rules/Pattern matching pipeline (lexer, parser, matcher), rule engine, command parser, CEL expression evaluator
exec/Command execution, platform-specific sandbox implementations, extension runner

runok supports three adapter types that share the same evaluation pipeline but differ in how they handle the result:

  • Exec (runok exec): Executes allowed commands directly (or via sandbox). Exits with code 3 for denied/ask actions.
  • Check (runok check): Performs dry-run evaluation and outputs the result as JSON or text. Always exits with code 0.
  • Hook: Integrates with LLM agent hook systems (e.g., Claude Code’s PreToolUse hook). Evaluates only Bash tool invocations and wraps allowed commands with runok exec --sandbox.