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).
Configuration File Locations
Section titled “Configuration File Locations”runok searches for configuration files in two scopes:
| Scope | Path | Purpose |
|---|---|---|
| Global | $XDG_CONFIG_HOME/runok/runok.yml | User-wide defaults |
| Project | <project>/runok.yml | Project-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:
| Scope | Override Path | Purpose |
|---|---|---|
| Global | $XDG_CONFIG_HOME/runok/runok.local.yml | Personal tweaks |
| Project | <project>/runok.local.yml | Personal project overrides |
Project Directory Discovery
Section titled “Project Directory Discovery”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` hereRunning 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.
File Extension Priority
Section titled “File Extension Priority”When both .yml and .yaml extensions exist, .yml takes priority:
runok.yml(preferred)runok.yaml(fallback)
This applies to both the main configuration file and the local override file in each scope.
Merge Order
Section titled “Merge Order”Configuration layers are merged bottom-to-top, with later layers taking higher priority:
| Priority | Layer | Path |
|---|---|---|
| 1 (low) | Global config | $XDG_CONFIG_HOME/runok/runok.yml |
| 2 | Global local override | $XDG_CONFIG_HOME/runok/runok.local.yml |
| 3 | Project config | <project>/runok.yml |
| 4 (high) | Project local override | <project>/runok.local.yml |
After merging all four layers, the resulting configuration is validated.
Merge Semantics
Section titled “Merge Semantics”Different fields use different merge strategies:
| Field | Strategy | Behavior |
|---|---|---|
extends | Append | Lists are concatenated. |
rules | Append | Rules from all layers are concatenated in order. |
defaults.action | Override | Higher-priority layer wins. |
defaults.sandbox | Override | Higher-priority layer wins. |
definitions.paths | Per-key append | Values for each key are concatenated (deduplicated). |
definitions.sandbox | Per-key override | Higher-priority layer replaces the entire preset. |
definitions.wrappers | Append | Lists are concatenated. |
definitions.commands | Append | Lists are concatenated. |
Example
Section titled “Example”Given a global config:
rules: - allow: 'git *'
definitions: paths: secrets: - ~/.sshAnd a project config:
rules: - allow: 'cargo build *' - deny: 'rm -rf /'
definitions: paths: secrets: - ~/.aws/credentialsThe merged result is:
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/credentialsPath Resolution
Section titled “Path Resolution”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 type | Example | Resolution |
|---|---|---|
| Absolute path | /etc/shadow | Used as-is |
| Home directory | ~/.ssh/** | ~ expanded to $HOME |
| Relative path | .env*, ./tmp | Joined 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.ymlare resolved relative to~/.config/runok/ - Paths in
<project>/runok.ymlare resolved relative to<project>/ - Paths in a preset loaded via
extendsare 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.
Validation
Section titled “Validation”After merging, runok validates the final configuration:
- Each rule must have exactly one of
deny,allow, orask. denyrules must not have asandboxattribute.sandboxvalues must reference names defined indefinitions.sandbox.<path:name>references infs.denymust resolve to entries indefinitions.paths.<path:name>references are not allowed insidedefinitions.pathsvalues.
All validation errors are collected and reported together so you can fix every issue in a single pass.
Related
Section titled “Related”- Configuration Schema — Full reference for
runok.ymlfields. - Extends (Presets) — Inherit configuration from other files or repositories.