Skip to content

File Discovery and Merging

runok loads configuration from up to four layers, merging them in a defined order. This allows you to set organization-wide defaults globally while overriding specific settings per project. For sharing configuration across repositories, see Extends (Presets).

runok searches for configuration files in two scopes:

ScopePathPurpose
Global$XDG_CONFIG_HOME/runok/runok.ymlUser-wide defaults
Project<project>/runok.ymlProject-specific rules

When XDG_CONFIG_HOME is not set, the global config directory defaults to ~/.config/runok/. See Environment Variables for details.

In each scope, an optional local override file is also loaded:

ScopeOverride PathPurpose
Global$XDG_CONFIG_HOME/runok/runok.local.ymlPersonal tweaks
Project<project>/runok.local.ymlPersonal project overrides

runok does not require you to run commands from the directory containing runok.yml. It automatically walks up from the current working directory, checking each ancestor for a configuration file (runok.yml, runok.yaml, runok.local.yml, or runok.local.yaml). The first directory that contains any of these files is used as the project configuration directory.

The traversal stops at the user’s home directory ($HOME). Configuration files placed directly in $HOME (e.g. ~/runok.yml) are not loaded as project configuration — use the global configuration instead.

For example, given this directory structure:

~/projects/myapp/
├── runok.yml ← project config found here
├── runok.local.yml
└── src/
└── lib/ ← you run `runok check` here

Running runok check from ~/projects/myapp/src/lib/ loads ~/projects/myapp/runok.yml and ~/projects/myapp/runok.local.yml.

If multiple ancestor directories contain configuration files, the nearest ancestor (closest to the current working directory) wins.

When both .yml and .yaml extensions exist, .yml takes priority:

  1. runok.yml (preferred)
  2. runok.yaml (fallback)

This applies to both the main configuration file and the local override file in each scope.

Configuration layers are merged bottom-to-top, with later layers taking higher priority:

PriorityLayerPath
1 (low)Global config$XDG_CONFIG_HOME/runok/runok.yml
2Global local override$XDG_CONFIG_HOME/runok/runok.local.yml
3Project config<project>/runok.yml
4 (high)Project local override<project>/runok.local.yml

After merging all four layers, the resulting configuration is validated.

Different fields use different merge strategies:

FieldStrategyBehavior
extendsAppendLists are concatenated.
rulesAppendRules from all layers are concatenated in order.
defaults.actionOverrideHigher-priority layer wins.
defaults.sandboxOverrideHigher-priority layer wins.
definitions.pathsPer-key appendValues for each key are concatenated (deduplicated).
definitions.sandboxPer-key overrideHigher-priority layer replaces the entire preset.
definitions.wrappersAppendLists are concatenated.
definitions.commandsAppendLists are concatenated.

Given a global config:

~/.config/runok/runok.yml
rules:
- allow: 'git *'
definitions:
paths:
secrets:
- ~/.ssh

And a project config:

./runok.yml
rules:
- allow: 'cargo build *'
- deny: 'rm -rf /'
definitions:
paths:
secrets:
- ~/.aws/credentials

The merged result is:

Merged result
rules:
# global rules come first, then project rules
- allow: 'git *'
- allow: 'cargo build *'
- deny: 'rm -rf /'
definitions:
paths:
secrets: # values are merged per-key
- ~/.ssh
- ~/.aws/credentials

Relative paths in definitions.paths, definitions.sandbox.*.fs.writable, and definitions.sandbox.*.fs.deny are resolved relative to the parent directory of the configuration file that defines them. This ensures consistent behavior regardless of the current working directory when running runok exec.

Paths are classified into three types:

Path typeExampleResolution
Absolute path/etc/shadowUsed as-is
Home directory~/.ssh/**~ expanded to $HOME
Relative path.env*, ./tmpJoined with the config file’s parent directory

Path resolution happens before merging, so each configuration file’s relative paths are resolved using its own parent directory. For example:

  • Paths in ~/.config/runok/runok.yml are resolved relative to ~/.config/runok/
  • Paths in <project>/runok.yml are resolved relative to <project>/
  • Paths in a preset loaded via extends are resolved relative to the preset file’s directory

. and .. components are normalized logically without filesystem access, so glob patterns (e.g. *.env*, **/.git) are preserved correctly.

After merging, runok validates the final configuration:

  • Each rule must have exactly one of deny, allow, or ask.
  • deny rules must not have a sandbox attribute.
  • sandbox values must reference names defined in definitions.sandbox.
  • <path:name> references in fs.deny must resolve to entries in definitions.paths.
  • <path:name> references are not allowed inside definitions.paths values.

All validation errors are collected and reported together so you can fix every issue in a single pass.