Sections

User Guide

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.

args is 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 to args everywhere 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:

{ "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:

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:

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:

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.

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
  }
}