RunnerMonitor is a lightweight terminal UI and command-line tool for monitoring and controlling CI runners. It was built for a workstation that has multiple GitHub Actions runners across Windows and WSL/Linux, with a planned move to a dedicated runner host on the local network.
The tool combines local runner lifecycle state with GitHub runner state, queue information, repository ownership, safe lifecycle commands, and read-only visibility for GitHub-hosted jobs and remote self-hosted runners.
- Discovers GitHub Actions runner folders from configured Windows, WSL, and Linux runner roots.
- Shows the project/repository each runner belongs to.
- Merges local service/process state with GitHub status from
gh api. - Shows queued and in-progress GitHub-hosted workflow jobs as read-only rows.
- Shows self-hosted runners registered in monitored GitHub repositories even when their runner folders are not local to this machine.
- Displays busy state, queued workflow count, and stale queued workflow count.
- Auto-refreshes TUI state every 5 seconds while keeping the previous table visible during refresh.
- Checks GitHub Releases once on TUI startup and shows a concise update notice when a newer version is available.
- Throttles repeated GitHub status lookups during TUI auto-refresh to reduce process and API overhead.
- Starts, stops, restarts, clears, removes, and reprovisions selected runners.
- Keeps destructive operations guarded by dry-runs, busy-runner checks, and explicit confirmation.
- Provides project-scoped commands that Codex or an operator can run before a push or CI wait.
- Supports saved SSH remote-runner host profiles for the future dedicated runner machine.
- Keeps OneDev support as a future provider-oriented direction.
- Go 1.26+
- Charmbracelet Bubble Tea for the TUI
- Charmbracelet Bubbles for table and text input widgets
- Charmbracelet Lip Gloss for terminal styling
- GitHub CLI (
gh) for GitHub Actions runner and workflow data - Windows PowerShell for Windows service discovery/control
- WSL and Linux systemd for Linux runner discovery/control
- SSH for remote runner-host access
-
Go 1.26+
-
GitHub CLI authenticated with access to monitored repositories:
gh auth status
-
Windows PowerShell for local Windows service discovery.
-
WSL for current-workstation Linux runner discovery.
-
gitfor current-project repository detection.
Download the latest ready-to-run Windows package from GitHub Releases:
RunnerMonitor-v0.6.0-windows-x64.zip
Extract the ZIP and start the TUI:
powershell -NoProfile -ExecutionPolicy Bypass -File .\runner-monitor.ps1Or build locally from source:
Build the executable and create the app-local config:
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\build.ps1
.\runner-monitor.ps1 --show-configOpen the TUI:
.\runner-monitor.ps1Print inventory once:
.\runner-monitor.ps1 --onceAudit runner cleanup candidates:
.\runner-monitor.ps1 --auditStart runners for the current GitHub project:
.\runner-monitor.ps1 --start-currentRunnerMonitor reads host-specific settings from runner-monitor.json next to
the executable. For the default local build, the config path is:
C:\Repos\RunnerMonitor\bin\runner-monitor.json
Create or inspect it:
.\runner-monitor.ps1 --init-config
.\runner-monitor.ps1 --show-config--show-config masks wslSudoPassword as <set> or <empty> and never prints
the actual value.
Default config:
{
"projectsRoot": "C:\\Repos",
"windowsRunnerRoots": [
"C:\\Runners"
],
"wslRunnerRoots": [
"/home/gsv777/Runners"
],
"linuxRunnerRoots": [
"/opt/Runners",
"/srv/Runners"
],
"githubHostedRepos": [],
"tuiRefreshIntervalSeconds": 5,
"wslSudoPassword": ""
}Settings fields:
| Field | Purpose |
|---|---|
projectsRoot |
Root directory used by --project, for example C:\Repos. |
windowsRunnerRoots |
Windows runner root folders to scan and treat as safe for runner-folder deletion. |
wslRunnerRoots |
WSL runner root folders to scan. |
linuxRunnerRoots |
Linux runner root folders used on a native Linux runner host. |
githubHostedRepos |
Extra owner/repo values or GitHub URLs whose GitHub-hosted workflow jobs should be monitored even when no local runner exists. |
tuiRefreshIntervalSeconds |
TUI auto-refresh interval in seconds. Defaults to 5 when omitted or invalid. |
wslSudoPassword |
Direct WSL sudo password value for fallback service control. Keep it only in the app-local config. |
For tests or special launch contexts, override the config path:
$env:RUNNER_MONITOR_CONFIG = "D:\Temp\runner-monitor.json"Do not commit a real runner-monitor.json or sudo password.
Inside the TUI:
Runner information auto-refreshes every 5 seconds by default. Change
tuiRefreshIntervalSeconds in runner-monitor.json to use a different
interval. The refresh command starts an immediate refresh; when existing data
is available, the table stays visible until the new data arrives. Automatic
refreshes reuse recent GitHub status briefly to avoid shelling out to gh for
every repository every 5 seconds; manual refresh still fetches fresh GitHub
state.
GitHub-hosted Actions jobs appear as github host rows with hosted local
state. RunnerMonitor discovers hosted jobs for repositories already represented
by local runners and for repositories listed in githubHostedRepos. These rows
are read-only: lifecycle, cleanup, remove, delete, and reprovision commands are
intentionally skipped because GitHub-hosted runners are ephemeral GitHub
infrastructure, not local machines.
Self-hosted runners returned by GitHub for monitored repositories also appear
when no matching local runner folder is discovered. These partner or otherwise
remote rows use github as host, remote as local state, and (not local) as
path. They expose GitHub status, busy state, labels, OS, version, and queue
counts. Local lifecycle, cleanup, logs, and reprovision commands are skipped
because RunnerMonitor cannot control another machine's runner folder or
service. remove [N] confirm and delete [N] confirm can unregister these
remote-only self-hosted runner registrations through GitHub API after explicit
confirmation; no local folder or service action is attempted.
On startup, the TUI performs one best-effort GitHub Releases check through
gh. If the internet is unavailable or the check fails, RunnerMonitor starts
normally and does not show an error. If a newer release exists, a short update
notice appears above the command input.
Press h or ?, or type help, to open the in-app help panel. It gives a
short command reference without leaving the TUI. Esc closes help first; when
help is closed, Esc exits the TUI.
| Command | Description |
|---|---|
h, ?, help |
Show or hide the in-app help panel. |
refresh |
Refresh local and GitHub runner state immediately. |
| Arrow keys | Select a runner row. |
start [N] |
Start runner N, or the selected runner if N is omitted. |
stop [N] |
Stop runner N, or the selected runner if N is omitted. |
start all |
Start every controllable runner in the current inventory. |
stop all |
Stop every controllable runner in the current inventory. |
restart [N] |
Restart runner N, or the selected runner if N is omitted. |
force-stop [N] |
Stop even when GitHub reports the runner as busy. Use carefully. |
force-restart [N] |
Restart even when GitHub reports the runner as busy. Use carefully. |
clear [N] |
Safely clear idle runner work files for runner N or selected row. |
clear idle |
Clear all idle runners. Busy runners are skipped. |
auto-clear on |
Run safe idle cleanup after refresh. |
auto-clear off |
Disable refresh-triggered auto cleanup. |
remove [N] |
Dry-run GitHub runner unregistration for runner N or selected row. |
remove [N] confirm |
Execute runner unregistration after confirmation. |
delete [N] confirm |
Unregister and delete a safe runner folder; for remote-only GitHub rows, unregister only. |
logs [N] |
Open runner logs for runner N or selected row. |
connect remote NAME |
Open the saved remote RunnerMonitor TUI over SSH. |
q, quit, exit, Esc, Ctrl+C |
Exit the TUI. |
The table is resize-aware. On narrow terminals, low-priority columns are hidden
before the main project, runner, status, busy, and queue columns are allowed to
drift. In the TUI table, the Busy column uses plain true/false text so
status columns stay aligned and readable.
After lifecycle commands such as start, stop, start all, stop all, and restart, the TUI starts
an immediate status refresh so the table catches up with the service and GitHub
state without waiting for the next auto-refresh tick. The command input accepts
d as the first typed character, so delete N confirm can be typed normally.
Inventory and audit:
.\runner-monitor.ps1 --once
.\runner-monitor.ps1 --auditProject-scoped lifecycle:
.\runner-monitor.ps1 --start-repo SGribanov/DeltaG
.\runner-monitor.ps1 --stop-repo SGribanov/DeltaG
.\runner-monitor.ps1 --restart-repo SGribanov/DeltaGSingle-runner lifecycle:
.\runner-monitor.ps1 --start-runner ideabox-runner --repo SGribanov/IdeaBox
.\runner-monitor.ps1 --stop-runner ideabox-runner --repo SGribanov/IdeaBox
.\runner-monitor.ps1 --restart-runner ideabox-runner --repo SGribanov/IdeaBoxCurrent Git project lifecycle:
.\runner-monitor.ps1 --start-current
.\runner-monitor.ps1 --stop-current
.\runner-monitor.ps1 --restart-currentstart for service-managed runners is a readiness check, not just a service
request. For systemd-backed runners, including WSL runners, RunnerMonitor
enables the unit, starts it, waits for the local service to become active, and
then waits until GitHub reports the runner as online.
stop for service-managed runners waits for the local service to stop and then
waits until GitHub reports the runner as offline. When a Windows service runner
is controlled from a non-elevated TUI, RunnerMonitor opens an elevated
PowerShell helper through UAC; after approval, the helper performs the stop and
the TUI keeps refreshing until the row catches up.
Safe cleanup:
.\runner-monitor.ps1 --clear-repo SGribanov/DeltaG
.\runner-monitor.ps1 --clear-current
.\runner-monitor.ps1 --clear-idle
.\runner-monitor.ps1 --clear-runner ideabox-runnerAutostart policy:
.\runner-monitor.ps1 --disable-autostartConfig:
.\runner-monitor.ps1 --init-config
.\runner-monitor.ps1 --init-config --overwrite-config
.\runner-monitor.ps1 --show-configRemote host profiles:
.\runner-monitor.ps1 --configure-remote runnerbox
.\runner-monitor.ps1 --connect-remote runnerboxRunner removal is dry-run by default:
.\runner-monitor.ps1 --remove-runner ideabox-runner --repo SGribanov/IdeaBox
.\runner-monitor.ps1 --remove-runner ideabox-runner --repo SGribanov/IdeaBox --confirm
.\runner-monitor.ps1 --remove-runner ideabox-runner --repo SGribanov/IdeaBox --confirm --delete-folderRunner reprovisioning configures an existing prepared runner distribution
folder. The --project value is a project folder name under configured
projectsRoot.
.\runner-monitor.ps1 --add-runner runner-monitor-win --project RunnerMonitor --runner-folder C:\Runners\SGribanov-RunnerMonitor\runner-monitor-win --labels "self-hosted,Windows,X64"
.\runner-monitor.ps1 --add-runner runner-monitor-win --project RunnerMonitor --runner-folder C:\Runners\SGribanov-RunnerMonitor\runner-monitor-win --labels "self-hosted,Windows,X64" --confirm --replaceInstall and start the runner service after configuration:
.\runner-monitor.ps1 --add-runner runner-monitor-win --project RunnerMonitor --runner-folder C:\Runners\SGribanov-RunnerMonitor\runner-monitor-win --labels "self-hosted,Windows,X64" --confirm --replace --serviceWhen runners move to a dedicated machine on the network, run RunnerMonitor on that machine and connect to it over SSH.
Configure or update a remote host profile:
.\runner-monitor.ps1 --configure-remote runnerboxThe prompt asks for:
- remote name;
- SSH host or alias;
- host OS:
windowsorlinux; - remote RunnerMonitor path;
- default remote project path.
Open the saved remote TUI:
.\runner-monitor.ps1 --connect-remote runnerboxEquivalent Windows SSH command:
ssh -t runnerbox "powershell -NoProfile -ExecutionPolicy Bypass -File C:/Repos/RunnerMonitor/runner-monitor.ps1"Start runners for the current remote project before Codex pushes or waits for CI:
ssh runnerbox "cd C:/Repos/DeltaG; powershell -NoProfile -ExecutionPolicy Bypass -File C:/Repos/RunnerMonitor/runner-monitor.ps1 --start-current"Linux remote host example:
ssh -t runnerbox "cd /opt/RunnerMonitor && ./runner-monitor"
ssh runnerbox "cd /srv/DeltaG && /opt/RunnerMonitor/runner-monitor --start-current"Saved remote profiles are stored separately from the app-local runner settings
under the current user's config directory as RunnerMonitor\remote-hosts.json.
The preferred current layout is:
C:\Runners\<owner>-<repo>\<runner-name>
/home/gsv777/Runners/<owner>-<repo>/<runner-name>
For a future dedicated Linux host, use a stable shared root such as:
/opt/Runners/<owner>-<repo>/<runner-name>
/srv/Runners/<owner>-<repo>/<runner-name>
Runner folder migration is tracked separately and should be performed runner-by-runner. Do not move or delete busy runners without explicit approval.
- Busy runners are protected by default.
- Removal and reprovisioning are dry-run by default.
- Folder deletion requires
--delete-folderand is limited to configured safe runner roots. - Folder deletion rejects configured root folders themselves and normalized path traversal.
- Cleanup removes safe generated content such as
_workcontents and runner installer archives, while preserving runner registration and binaries. --show-confignever prints the real WSL sudo password.- Local app config files and secrets must not be committed.
- GitHub runner registration/remove token output is redacted from command errors; token passing is kept out of parent process arguments where the runner config scripts allow it.
Run tests:
go test ./...Build:
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\build.ps1Generate icon assets:
uv run --with pillow python .\scripts\generate-icon-assets.py
go run github.com/akavel/rsrc@v0.10.2 -arch amd64 -ico .\assets\runner-monitor-hourglass.ico -o .\cmd\runner-monitor\runner-monitor_windows_amd64.sysocmd/runner-monitor/ Application entry point
internal/app/ Discovery, GitHub integration, lifecycle, TUI, cleanup
scripts/ Build, migration, cleanup, and automation helpers
assets/ Hourglass icon assets
reports/ Runner audit reports
research/ Long-lived project insights
tasks/ Per-issue implementation plans and status files
See CONTRIBUTING.md. Contributions should preserve the safety model: no unguarded destructive runner operations, no committed secrets, and no silent changes to runner registrations. Please also follow the Code of Conduct.
See SECURITY.md. Do not open public issues with secrets, runner tokens, sudo passwords, or private hostnames.
RunnerMonitor is licensed under the MIT License.