config.json Reference
Every field of Runyard's config.json file, with examples.
Runyard reads a single JSON file that describes your tools. This page documents every field.
The config file lives at ~/Library/Application Support/Runyard/config.json by default. You can move it to a synced folder. See Syncing Across Macs.
Top-level structure
{
"tools": [ /* ... */ ],
"paths": ["/opt/homebrew/bin"],
"advanced": { /* optional timing settings */ }
}
| Field | Type | Default | Description |
|---|---|---|---|
tools |
array | [] |
List of tool definitions (see below). |
paths |
string[] | [] |
Directories prepended to PATH when spawning any command. Listed in priority order. ~ is expanded. |
advanced |
object | - | Timing settings (see Advanced settings). |
Launch at login is not stored in
config.json. Toggle it in Settings → General → Launch Runyard at login; macOS persists the choice via the system Login Items list.
Tool definition
Each entry in tools is one tool.
| Field | Type | Default | Description |
|---|---|---|---|
name |
string | required | Display name shown in the menu. |
type |
string | required | "service", "shortcut", "group", or "healthCheck" (see Tool types). |
directory |
string | required* | Service only. Project root (~ expanded). Commands run from here unless workingDir is set. Ignored on other tool types. |
startCommands |
array | required* | Processes to spawn (see Start commands). |
stopCommands |
array | - | Custom shutdown commands (see Stop commands). |
actions |
array | - | Extra menu items (see Actions). |
tools |
array | - | Nested tools inside a group. Services and shortcuts only; no nested groups. |
autoStart |
boolean | false (service) / true (health check) |
Start this tool automatically when Runyard launches. Defaults to false for services; health checks default to true (they poll unless you set it to false). |
keepSystemAwake |
boolean | false |
When the service is running, prevent the system from sleeping. See Keep awake. |
keepDisplayAwake |
boolean | false |
When the service is running, also prevent the display from sleeping. Has no effect unless keepSystemAwake is also true. |
installCommand |
object | - | Command to run if the marker path is missing (see Install command). |
paths |
string[] | - | Additional PATH entries for this tool. |
pathsOverride |
boolean | false |
If true, paths replaces global paths entirely instead of merging. |
* directory and startCommands are required for service tools. directory is service-only — it is ignored on shortcut, group, and healthCheck tools and dropped from the file when saved.
Tool types
| Type | Renders as | Required fields |
|---|---|---|
"service" |
Bold header + Start/Stop + state dot + logs + actions. | directory, startCommands |
"shortcut" |
Bold header + list of actions. No process management. | actions (at least one) |
"group" |
Submenu with nested tools and/or actions. | actions OR tools (at least one) |
"healthCheck" |
Status dot + periodic HTTP/TCP poll (see Health Checks). | http OR tcp (exactly one) |
Nested services inside a group behave exactly like top-level services. Nested groups are not allowed.
Start commands
Each entry in startCommands is one process to spawn.
| Field | Type | Default | Description |
|---|---|---|---|
label |
string | required | Unique name for this process. |
command |
string | required | Executable to run (e.g., npm, docker-compose). |
args |
string[] | [] |
Arguments passed to the command. |
workingDir |
string | tool directory | Working directory, relative to directory. |
startupCheck |
string | - | HTTP URL to poll until it returns a 2xx or 3xx during service startup. |
startupFallbackPort |
number | - | Fallback port if auto-detection fails. Also a hint when multiple ports are detected. |
startupRequestTimeout |
number | global | Per-request HTTP timeout (seconds) for startup polling. Overrides advanced.startupRequestTimeout for this process only. |
waitFor |
string | - | Label of another process that must be healthy before this one starts. |
argsis a list of separate arguments, not a command line. Each entry is passed to the command verbatim as one argument; Runyard never runs it through a shell, so it never splits an entry on spaces. Put each flag or value in its own entry —["run", "dev"], not["run dev"]. An entry may contain spaces when it is meant to reach the program as a single argument (for example"--command=zsh -lc 'npm start'"). This applies toargseverywhere it appears: start commands, stop commands, command actions, and the install command.
Port-agnostic startup checks
If startupCheck is a localhost URL without an explicit port (e.g., http://localhost/api/health), Runyard injects the detected port at startup. This means your config works even when the dev server picks a different port each run.
A URL with an explicit port (e.g., http://localhost:3001/health) is used as-is.
Process dependencies with waitFor
"startCommands": [
{ "label": "Backend", "command": "npm", "args": ["run", "dev"], "startupFallbackPort": 3001 },
{ "label": "Frontend", "command": "npm", "args": ["run", "dev"], "workingDir": "frontend",
"startupFallbackPort": 5173, "waitFor": "Backend" }
]
Frontend won't start until Backend's startup check passes.
Stop commands
Optional. When stopCommands is set, Runyard runs each command sequentially instead of sending SIGTERM. After they finish, any lingering processes are force-killed as a safety net.
Uses the same shape as startCommands. Fields like startupCheck, startupFallbackPort, and waitFor are ignored.
"stopCommands": [
{ "label": "Compose Down", "command": "docker-compose", "args": ["down"] }
]
When stopCommands is absent, the default shutdown is SIGTERM → grace period → SIGKILL.
Actions
Actions add custom items to a tool's menu. The required type field selects the action kind: "url", "command", "reveal", "applescript", "applescriptFile", or "healthCheck". The payload field matching that type is required; any other payload field is rejected by validation.
| Field | Type | Default | Description |
|---|---|---|---|
label |
string | required | Menu item text. |
type |
string | required | One of "url", "command", "reveal", "applescript", "applescriptFile", "healthCheck". |
url |
string | - | URL to open in the default browser. |
command |
string | - | Shell command to run. |
args |
string[] | [] |
Arguments for the shell command. |
workingDir |
string | tool directory | Working directory for the command. On a service, relative to the tool directory; on a shortcut/group (no directory), use an absolute or ~ path. |
reveal |
string | - | Path to open in Finder. On a service, a relative path resolves against the tool directory; on a shortcut/group, use an absolute or ~ path. |
applescript |
string | - | Inline AppleScript source. |
applescriptFile |
string | - | Path to a .applescript file. |
showWhen |
string | "running" |
"always", "running", or "stopped" (service only). |
confirm |
boolean | false |
When true, Runyard asks for confirmation before running the action. |
Action examples
Open a URL:
{ "label": "Open Frontend", "type": "url", "url": "http://localhost:5173" }
Run a shell command:
{ "label": "Seed Database", "type": "command", "command": "npm", "args": ["run", "db:seed"] }
Open a file in a specific app:
{ "label": "Edit README", "type": "command", "command": "open", "args": ["-a", "TextEdit", "./README.md"], "showWhen": "always" }
Open a project in Cursor:
{ "label": "Open in Cursor", "type": "command", "command": "cursor", "args": ["."], "showWhen": "always" }
Reveal a folder:
{ "label": "Open Project", "type": "reveal", "reveal": ".", "showWhen": "always" }
{ "label": "Open Backend Logs", "type": "reveal", "reveal": "backend/logs", "showWhen": "always" }
Inline AppleScript:
{ "label": "Activate Safari", "type": "applescript", "applescript": "tell application \"Safari\" to activate", "showWhen": "always" }
Multiline AppleScript (use \n):
{
"label": "Focus Safari",
"type": "applescript",
"applescript": "try\ntell application \"System Events\" to tell process \"Safari\"\nset frontmost to true\nend tell\nend try",
"showWhen": "always"
}
AppleScript from a file:
{ "label": "Deploy", "type": "applescriptFile", "applescriptFile": "scripts/deploy.applescript", "showWhen": "always" }
Port placeholders
Action URLs support two placeholders:
{{port}}: the tool's last process's detected port.{{port:Label}}: a specific process's port, by label.
{ "label": "Open API Docs", "type": "url", "url": "http://localhost:{{port:Backend}}/api/docs" }
If the port isn't known yet (tool not running), the placeholder is left in place and the URL won't open.
Confirmation prompt
Set confirm to true on any action and Runyard will show a confirmation alert before running it. Useful for destructive actions like killing dev servers, dropping a database, or wiping caches.
{
"label": "Kill Stale Dev Servers",
"type": "command",
"command": "pkill",
"args": ["-f", "next dev"],
"confirm": true,
"showWhen": "always"
}
The alert's primary button runs the action; the default button is Cancel, so an accidental Return keypress doesn't fire it.
showWhen behaviour
| Value | When shown |
|---|---|
"running" (default) |
Tool is running. |
"stopped" |
Tool is stopped or errored. |
"always" |
Always shown. |
showWhen only applies to service tools. On shortcut and group tools, actions are always visible.
Install command
Runs once before the first Start, if a marker path doesn't exist. Useful for npm install and friends.
"installCommand": {
"command": "npm",
"args": ["install"],
"markerPath": "node_modules",
"enabled": true
}
| Field | Type | Default | Description |
|---|---|---|---|
command |
string | required | Executable. |
args |
string[] | [] |
Arguments. |
markerPath |
string | "node_modules" |
Path (relative to tool directory) checked before running. If it exists, install is skipped. |
enabled |
boolean | true |
When false, the install command is preserved in the JSON but skipped at runtime. Lets you temporarily disable the install step without losing the configuration. |
Three-state behavior
| Config | UI state | Runtime |
|---|---|---|
installCommand omitted |
"Enable auto-install" off, fields empty/placeholder | Never configured; no install runs |
"installCommand": { …, "enabled": false } |
"Enable auto-install" off, fields populated and grayed | Configured but skipped; user data preserved |
"installCommand": { …, "enabled": true } (or omitted enabled) |
"Enable auto-install" on, fields populated | Configured and runs at startup if marker missing |
The Settings → Tools → Install Command toggle binds to enabled. Toggling it off does not delete the rest of the configuration; it just sets enabled: false.
Advanced settings
All fields are optional. Defaults shown.
"advanced": {
"startupTimeout": 30.0,
"startupPollInterval": 1.0,
"startupRequestTimeout": 5.0,
"sigTermGracePeriod": 3.0,
"installTimeout": 300.0,
"stopCommandTimeout": 30.0,
"logMaxFileSizeMB": 5,
"logKeepRotations": 3,
"logMaxAgeDays": 30
}
| Field | Default | Description |
|---|---|---|
startupTimeout |
30.0 |
Total seconds to wait for a process to pass its startup check before marking it errored. |
startupPollInterval |
1.0 |
Seconds between startup check polls. |
startupRequestTimeout |
5.0 |
Seconds to wait for a single startup check HTTP request. Individual processes can override this with startupRequestTimeout on the start command. |
sigTermGracePeriod |
3.0 |
Seconds between SIGTERM and SIGKILL when stopping a process without stopCommands. |
installTimeout |
300.0 |
Seconds to wait for the install command before aborting. |
stopCommandTimeout |
30.0 |
Seconds to wait for each stopCommands entry. |
logMaxFileSizeMB |
5 |
Size (MB) a log file can reach before Runyard rotates it. Rotated files are renamed *.log.1, *.log.2.gz, etc. and gzipped past the first rotation. Range: 1–100. |
logKeepRotations |
3 |
Number of older rotations retained on disk for each log stream. Older files are deleted at the next rotation. Range: 1–20. |
logMaxAgeDays |
30 |
Rotated log files older than this are deleted when the app launches. The live .log file is never deleted by age. Range: 1–365. |
Paths and PATH resolution
Commands run through /usr/bin/env with a custom-built PATH. The rules:
- Global
pathsare included by default. - Tool-level
pathsare merged on top of global paths (global first, then tool). - Set
pathsOverride: trueon a tool to replace global paths entirely. Use this only when a tool conflicts with global binaries.
A typical setup:
{
"paths": ["~/.nvm/versions/node/v22.0.0/bin", "/opt/homebrew/bin"]
}
For asdf users, add ~/.asdf/shims (and /opt/homebrew/bin if asdf was installed via Homebrew):
{
"name": "My Phoenix App",
"type": "service",
"directory": "~/Code/my-app",
"paths": ["~/.asdf/shims", "/opt/homebrew/bin"],
"startCommands": [
{ "label": "Server", "command": "mix", "args": ["phx.server"],
"startupCheck": "http://localhost:4000/health", "startupFallbackPort": 4000 }
]
}
Health Checks
A health check is a tool type that polls an arbitrary endpoint on its own interval. Health checks are independent of any service Runyard manages: you can target a remote API, a local database, or anything that speaks HTTP or accepts a TCP connection.
Health checks appear in the popover as their own collapsible cards (when defined at the top level) or as compact rows (when nested inside a group). The state pill and live status dot tell you what's going on:
- Green dot / Healthy: last poll succeeded.
- Red dot / Failing: 2+ consecutive polls failed (configurable via
failureThreshold). - Grey dot / Paused: polling is suspended, or the first poll hasn't completed yet (Unknown).
When any health check is failing, a small red warning triangle appears next to the Runyard menu bar icon. Groups that contain the failing health check also flip their aggregate pill into the warning state.
Health-check schema
{
"name": "Production API",
"type": "healthCheck",
"autoStart": true, // optional, default true
"interval": 30, // optional, seconds; default 30, minimum 5
"failureThreshold": 2, // optional, consecutive failed polls before marking failing; default 2, minimum 1
// HTTP check (mutually exclusive with tcp)
"http": {
"url": "https://api.example.com/health",
"expectStatus": 200, // optional; int or [int]; default 200
"expectBodyContains": "ok", // optional; case-sensitive substring
"requestTimeout": 5 // optional; seconds; default 5
},
// TCP check (mutually exclusive with http)
"tcp": {
"host": "db.internal",
"port": 5432,
"connectTimeout": 3 // optional; seconds; default 3
}
}
A health check must specify exactly one of http or tcp.
Failure threshold
A health check flips to failing after failureThreshold consecutive failed polls (default 2), and back to healthy after 1 successful poll. The threshold avoids flapping on transient network blips while still surfacing real outages within one polling interval. Set failureThreshold: 1 for an aggressive health check that flips on the first failure, or a higher value for more tolerance.
Pause / resume from the popover
Use Pause / Resume to suspend or resume polling. On a top-level health-check card, the button lives in the card's footer button-bar. On a nested row inside a group, it's an icon button on the right edge of the row. Pause and Resume are runtime-only: they do not modify config.json, so the probe respects the autoStart field on next launch. The popover does not close.
Per-health-check controls
A top-level health-check card shows:
- A header with the health-check name and a Healthy / Failing / Paused / Unknown pill.
- A meta-line with the latest latency, a "checked Xs ago" relative timestamp, and the polling interval (e.g.,
247 ms · checked 12s ago · every 30s). - A 20-bar latency sparkline of the most recent samples: green for successes, full-height red for failures.
- A footer with Pause / Resume, Check now (runs an out-of-band poll without affecting the regular timer), and Logs (opens
~/Library/Logs/Runyard/{HealthCheckName}.login Console.app).
Right-click a nested row to copy the endpoint URL (http://...) or host:port to the clipboard.
Latency stats (p50 / p95 / average)
Hover the status pill or the sparkline to open the stats popover: p50 (median), p95 (slow tail), average latency, and the failure rate. Stats are computed over a rolling 1-hour window of recent polls. The window is fixed and not configurable, picked to give enough samples for a meaningful p95 at any reasonable polling interval (5 s to 60 s) while bounding memory use.
The window is in-memory only and resets when Runyard quits. Until five successful polls land after a (re)start or after a long offline gap, the popover shows a "few samples" view with the most recent latency instead of the full breakdown.
Health checks inside groups
A group tool's tools array can contain healthCheck entries alongside services and shortcuts. Failing nested health checks flip the group's aggregate pill into the warning state. Nested rows render compactly without the sparkline. Promote one to top-level if you want the sparkline + full card UI.
One-shot healthCheck action
Any tool's actions array can include an action whose type is healthCheck. Clicking it runs an ad-hoc check and updates the action's label inline for ~3 seconds with the result.
"actions": [
{
"label": "Verify Staging",
"type": "healthCheck",
"healthCheck": {
"http": {
"url": "https://staging.example.com/health",
"expectStatus": 200
}
}
}
]
The healthCheck action shape mirrors a health check's http or tcp block, minus runtime fields (interval, autoStart).
Keep awake
Set keepSystemAwake: true on a service to prevent macOS from sleeping while that service is running. Runyard spawns one caffeinate -i -w <pid> per running start-process, so the system stays awake while any of the service's processes is alive. Each per-process assertion releases the moment its watched PID disappears (graceful exit, SIGKILL, or crash), and the service-level assertion drops only once every start-process has exited. Order of startCommands doesn't matter.
Add keepDisplayAwake: true to also pass -d, which prevents the display from dimming or locking. It's opt-in because it drains the battery faster.
{
"name": "Big Build",
"type": "service",
"directory": "~/Code/big-build",
"keepSystemAwake": true, // system stays awake while running
"keepDisplayAwake": false, // display still dims (default)
"startCommands": [
{ "label": "Build", "command": "npm", "args": ["run", "build"] }
]
}
The same flags work on healthCheck tools. A probe has no PID, so Runyard holds an indefinite caffeinate -i for as long as the probe is enabled (not paused) and releases it the moment you pause the probe, reload it out of the config, or flip the flag off.
{
"name": "Prod Status",
"type": "healthCheck",
"keepSystemAwake": true, // hold caffeinate while polling
"interval": 30,
"http": { "url": "https://api.example.com/healthz", "expectStatus": 200 }
}
The same caffeinate plumbing also drives the popover's Keep Awake toggle, a manual safety net you can flip on without editing config. Manual, service-bound, and probe-bound activations can be active at the same time; each holds its own independent power assertion.
Limitation: macOS does not keep your Mac awake when the lid is closed, regardless of which sleep-prevention tool you use. This is a thermal-safety constraint and applies to any caffeinate-based tool.
Menu bar popover
The popover is the only menu-bar UI. A few user-defaults keys back its behavior:
UserDefaults key |
Type | Default | Description |
|---|---|---|---|
collapsed.{toolName} |
Bool |
false |
Per-card collapsed state. Written when you click a card's chevron or the header's Collapse all / Expand all toggle. Restored across app launches. |
popoverShowSummary |
Bool |
true |
Hides or shows the N running · M errors summary line in the popover header. |
popoverDefaultState |
String |
"lastUsed" |
Default collapse state applied to every top-level card each time the popover opens. Set by the segmented picker in Settings → General → Popover. "expanded" forces all cards open on every open; "collapsed" forces them closed; "lastUsed" preserves per-card memory. Manual collapses during a session are kept until the next open, then reset (except in "lastUsed" mode). |
These keys live in ~/Library/Preferences/ca.bonevil.runyard.plist. Edit them with defaults write ca.bonevil.runyard <key> <value> if you ever need to nuke the layout from the command line.
The popover is sticky by default. It only dismisses on Escape, an outside click, or focus loss.
Full example
{
"paths": ["~/.nvm/versions/node/v22.0.0/bin", "/opt/homebrew/bin"],
"tools": [
{
"name": "MyApp",
"type": "service",
"directory": "~/Code/my-app",
"autoStart": true,
"installCommand": { "command": "npm", "args": ["install"] },
"startCommands": [
{ "label": "Backend", "command": "npm", "args": ["run", "dev"],
"startupCheck": "http://localhost/api/health", "startupFallbackPort": 3001 },
{ "label": "Frontend", "command": "npm", "args": ["run", "dev"],
"workingDir": "frontend", "startupFallbackPort": 5173, "waitFor": "Backend" }
],
"actions": [
{ "label": "Open Frontend", "type": "url", "url": "http://localhost:{{port}}/" },
{ "label": "API Docs", "type": "url", "url": "http://localhost:{{port:Backend}}/api/docs" },
{ "label": "Seed Database", "type": "command", "command": "npm", "args": ["run", "db:seed"] },
{ "label": "Open in Cursor", "type": "command", "command": "cursor", "args": ["."], "showWhen": "always" },
{ "label": "Open Project", "type": "reveal", "reveal": ".", "showWhen": "always" }
]
},
{
"name": "Quick Links",
"type": "shortcut",
"actions": [
{ "label": "Grafana", "type": "url", "url": "https://grafana.example.com" },
{ "label": "Jira", "type": "url", "url": "https://jira.example.com" }
]
},
{
"name": "Production API",
"type": "healthCheck",
"interval": 30,
"failureThreshold": 2,
"http": {
"url": "https://api.example.com/health",
"expectStatus": 200,
"expectBodyContains": "ok"
}
}
],
"advanced": {
"startupTimeout": 45.0,
"startupPollInterval": 1.0
}
}