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 | "service" |
"service", "shortcut", or "group" (see Tool types). |
directory |
string | required* | Project root (~ expanded). Commands run from here unless workingDir is set. |
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 |
Start this tool automatically when Runyard launches (service only). |
installCommand |
object | — | Command to run if the marker path is missing (see Install command). |
keepMenuOpen |
boolean | false |
Keep the menu open when clicking Start/Stop (service only). |
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, optional for shortcut/group.
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) |
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. |
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. Every action has exactly one type: url, command, reveal, applescript, or applescriptFile.
| Field | Type | Default | Description |
|---|---|---|---|
label |
string | required | Menu item text. |
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. |
reveal |
string | — | Path to open in Finder (relative to tool directory, or absolute/~). |
applescript |
string | — | Inline AppleScript source. |
applescriptFile |
string | — | Path to a .applescript file. |
showWhen |
string | "running" |
"always", "running", or "stopped" (service only). |
keepMenuOpen |
boolean | false |
Keep the menu open when clicking this action. |
Action examples
Open a URL:
{ "label": "Open Frontend", "url": "http://localhost:5173" }
Run a shell command:
{ "label": "Seed Database", "command": "npm", "args": ["run", "db:seed"] }
Open a file in a specific app:
{ "label": "Edit README", "command": "open", "args": ["-a", "TextEdit", "./README.md"], "showWhen": "always" }
Open a project in Cursor:
{ "label": "Open in Cursor", "command": "cursor", "args": ["."], "showWhen": "always" }
Reveal a folder:
{ "label": "Open Project", "reveal": ".", "showWhen": "always" }
{ "label": "Open Backend Logs", "reveal": "backend/logs", "showWhen": "always" }
Inline AppleScript:
{ "label": "Activate Safari", "applescript": "tell application \"Safari\" to activate", "showWhen": "always" }
Multiline AppleScript (use \n):
{
"label": "Focus Safari",
"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", "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", "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.
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"
}
| 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. |
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
}
| 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. |
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",
"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 probe is a tool type that polls an arbitrary endpoint on its own interval. Probes are independent of any service Runyard manages — you can probe a remote API, a local database, or anything that speaks HTTP or accepts a TCP connection.
Probes appear as rows in the menu with a live status dot:
- Green dot — last poll succeeded.
- Red dot — 2+ consecutive polls failed.
- Grey dot — paused, or first poll hasn't completed yet.
When any probe is failing, a small red warning triangle appears next to the Runyard menu bar icon, and any group containing the failing probe gets the same triangle next to its name in the dropdown.
Probe schema
{
"name": "Production API",
"type": "probe",
"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 probe (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 probe (mutually exclusive with http)
"tcp": {
"host": "db.internal",
"port": 5432,
"connectTimeout": 3 // optional; seconds; default 3
}
}
A probe must specify exactly one of http or tcp.
Failure threshold
A probe 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 probe that flips on the first failure, or a higher value for more tolerance.
Pause / resume from the menu
Click the pause/play icon on the right edge of a probe row to suspend or resume polling. The new state is written to your config.json (the autoStart field). The menu does not close.
Per-probe submenu
Each probe row has a submenu containing:
- The endpoint URL or
host:port(click to copy). - The last-check timestamp and outcome.
- The last error message (if currently failing).
- Check Now — runs an out-of-band poll and updates the row immediately. The regular polling timer is unaffected.
- View Logs — opens the per-probe log file at
~/Library/Logs/Runyard/{ProbeName}.log.
Probes inside groups
A group tool's tools array can contain probe entries alongside services and shortcuts. Failing probes inside a group show as a red triangle on the group's parent menu item.
One-shot healthCheck action
Any tool's actions array can include an action whose type is healthCheck. Clicking it runs an ad-hoc probe and updates the action's label inline for ~3 seconds with the result.
"actions": [
{
"label": "Verify Staging",
"healthCheck": {
"http": {
"url": "https://staging.example.com/health",
"expectStatus": 200
}
}
}
]
The healthCheck action shape mirrors a probe's http or tcp block, minus runtime fields (interval, autoStart).
Full example
{
"paths": ["~/.nvm/versions/node/v22.0.0/bin", "/opt/homebrew/bin"],
"tools": [
{
"name": "MyApp",
"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", "url": "http://localhost:{{port}}/" },
{ "label": "API Docs", "url": "http://localhost:{{port:Backend}}/api/docs" },
{ "label": "Seed Database", "command": "npm", "args": ["run", "db:seed"] },
{ "label": "Open in Cursor", "command": "cursor", "args": ["."], "showWhen": "always" },
{ "label": "Open Project", "reveal": ".", "showWhen": "always" }
]
},
{
"name": "Quick Links",
"type": "shortcut",
"actions": [
{ "label": "Grafana", "url": "https://grafana.example.com" },
{ "label": "Jira", "url": "https://jira.example.com" }
]
}
],
"advanced": {
"startupTimeout": 45.0,
"startupPollInterval": 1.0
}
}