Install
$ agentstack add skill-osouthgate-agent-plus-skills-railway-ops ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ● Shell / process execution Used
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
railway-ops
Project-scoped CLI that wraps the Railway CLI into a read-first, JSON-output overview tool. Stdlib-only Python 3. Designed for incident triage — one call returns the full project/services/errors/envs picture so you don't burn context chaining per-service railway invocations.
Lives at ${CLAUDE_SKILL_DIR}/../../bin/railway-ops; the plugin auto-adds bin/ to PATH, so just run railway-ops ....
Prerequisites
railwayCLI installed and on PATH (railway --versionmust succeed).- Authenticated —
railway loginmust have run (railway whoamimust return a user). - Project linked —
railway linkmust have been run in this repo (or pass--envexplicitly). - Optional:
RAILWAY_API_TOKEN(orRAILWAY_TOKEN) env var — unlocks deploy history via Railway's GraphQL API, giving youactiveDeploy(currently serving) separately fromlatestDeploy(most recent attempt), plus commit SHA, PR number, branch, and timestamps on each. Without a token the skill still works — it just falls back to a single CLI-sourced deploy with onlyid/status.
The skill bails with a clear message if any of these preconditions are missing.
When to reach for this
- User asks "what's happening in prod" — run
overview --env production --prettyand you get services, deploy status, recent errors, and env var names in one shot. - User asks "why is broken" — run
errors --env production --prettyfor focused error/warning triage. - User asks "what env vars does have" — run
envsto get NAMES only. Values never touch stdout. - User says "show me Railway" / "Railway status" — run
statusto get the project/env/whoami context.
When NOT to use this — fall back to railway directly
This wrapper is read-only by design. Write actions are deliberately unwrapped and rejected (up, deploy, redeploy, restart, down, delete, init, link, unlink, add, scale). If the user wants to DO something (change state), you should skip railway-ops entirely and use the raw railway CLI — it's already authed on their machine.
Specific cases where you should use railway ... (or gh / git / a deploy hook) directly, not railway-ops:
- Redeploying, restarting, or triggering a fresh build. →
railway redeploy,railway up, or push a commit. - Changing env vars (set/unset/import). →
railway variables --set KEY=VALUEor the Railway dashboard.railway-ops envsonly reads NAMES. - Linking or switching environments. →
railway link,railway environment. - Reading the raw build log of a SUCCESSFUL deploy.
railway-ops overviewauto-attachesbuildLogTailonly whenlatestDeployis FAILED and distinct fromactiveDeploy. For the build log of a successful deploy, either pass--deploymenttorailway-ops build-logs(which works on any status), or runrailway logs --deploymentdirectly. - Tailing logs live.
railway-opsdoes one-shot JSON snapshots; it doesn't stream. Userailway logs -sfor a live tail. - Anything the wrapper doesn't expose yet — volumes, plugins, teams, billing, domain config. The wrapper scope is deliberately narrow; everything else is
railwayterritory.
Don't get stuck in a loop. If a railway-ops command returns a "blocked write subcommand" error, or the user's request obviously needs a write the wrapper doesn't support, immediately switch to railway directly rather than re-trying railway-ops with different flags. The wrapper's purpose is to make reading faster and safer, not to replace the CLI.
Triage recipes
DB incident on a known service — skip overview, go straight to the focused command with a high limit:
railway-ops errors --env production --since 24h --limit 50 --pretty
overview caps each service's errors[] at 20 and is noisy when you already know which service is on fire. errors pulls 10× the log lines, caps errors/warnings at 50 by default, and emits a bucketed errorKinds summary (fingerprint → count) so a flood of 800 identical FK violations can't hide behind the truncation. Read errorTotal and errorKinds first — they reveal scale before you read any individual line.
Scanning the whole env — use overview, but always check summary.errors and each service's errorTotal / errorKinds before trusting the per-service errors[] list. errors[] is truncated; the kinds buckets are not.
"Is prod serving or broken?" — check both activeDeploy AND latestDeploy. activeDeploy=SUCCESS 23 hours ago + latestDeploy=FAILED 8 minutes ago means traffic is fine, someone just tried to ship and the build failed — completely different triage from "prod is down." If activeDeploy is null, the GraphQL path wasn't available (no RAILWAY_API_TOKEN) or the service has never had a successful deploy.
"Why did the failed deploy fail?" — when latestDeploy.status is FAILED/CRASHED/ERRORED AND it's a different deploy from activeDeploy, the overview auto-includes a buildLogTail (last ~30 build-log lines), a buildErrorKinds fingerprint summary, and buildLineCount right on the latestDeploy object. One call tells the whole story: active is still serving, this newer attempt failed, here's why. Look at buildErrorKinds first — "Build Failed: failed to compute cache key: ... not found": 1 pinpoints the Docker layer that broke without scrolling.
"Errors since my current deploy came up" — errors --since-deploy scopes to logs from the active deploy's createdAt onward, so you don't see noise from a previous version. Needs RAILWAY_API_TOKEN; falls back to --since with a stderr warning if unavailable.
Commands
All commands emit a single JSON document to stdout (the "JSON-first contract"). Use --pretty for indented output when a human is reading; default is compact JSON for piping into jq or agent tooling.
# Single-call snapshot — the headline feature. Project, env, per-service
# deploy status, recent errors + warnings (last 24h by default), env var
# NAMES per service. Runs per-service fetches in parallel.
railway-ops overview --env production --pretty
railway-ops overview --env staging --since 1h --pretty
# Narrow to a single service (case-insensitive substring match against name)
railway-ops overview --env production --service api --pretty
# Override the per-service errors/warnings cap (default 20)
railway-ops overview --env production --limit 50 --pretty
# whoami + linked project + available environments
railway-ops status --pretty
# Identity for `agent-plus refresh` ("what's my railway identity") — soft-fails
# (exit 0 + nulls) when CLI is missing, not logged in, or no linked project.
railway-ops whoami --json
# One service's errors + warnings (deeper than overview — bigger --limit)
railway-ops errors api --env production --since 2h --limit 50 --pretty
# Scope to logs since the active deploy came up (needs RAILWAY_API_TOKEN)
railway-ops errors api --env production --since-deploy --pretty
# Env var NAMES only for one service. Values are stripped at parse time and
# never reach stdout or stderr.
railway-ops envs api --env production --pretty
# Short per-service deploy-status list
railway-ops services --env production --pretty
# All Railway projects visible to this account
railway-ops projects --pretty
Hard safety rules (non-negotiable)
- Env var VALUES never touch stdout or stderr. The tool calls
railway variables --json, parses the{KEY: VALUE}dict, keeps only the keys, and drops the dict before emitting. If the agent needs a specific value for a troubleshooting task, the user runsrailway variablesdirectly — the skill exists specifically to prevent accidental value leakage into conversation transcripts. - Read-only. Write subcommands (
up,deploy,redeploy,restart,down,delete,init,link,unlink,add,scale) are rejected before reaching therailwaybinary. - Prerequisites are checked at startup. If
railway --versionfails, orrailway whoamidoesn't show a logged-in user, the tool exits with a clear remediation message.
overview output shape
{
"project": "loamdb",
"projectId": "5199ef24-...",
"env": "production",
"since": "24h",
"filter": null,
"summary": {
"services": 3,
"failures": 0,
"errors": 2,
"warnings": 5
},
"services": [
{
"name": "api",
"id": "e2b67796-...",
"status": "SUCCESS",
"stopped": false,
"activeDeploy": {
"id": "ddf2184d-...",
"status": "SUCCESS",
"createdAt": "2026-04-23T14:00:00Z",
"updatedAt": "2026-04-23T14:02:30Z",
"staticUrl": "https://api-production.up.railway.app",
"commitSha": "abc123",
"commitMessage": "release: v0.0.4.5",
"prNumber": 639,
"branch": "main"
},
"latestDeploy": {
"id": "c8000cda-...",
"status": "FAILED",
"createdAt": "2026-04-24T13:45:00Z",
"updatedAt": "2026-04-24T13:45:15Z",
"staticUrl": null,
"commitSha": "def456",
"commitMessage": "hotfix: stop dropping relationship_evidence FK",
"prNumber": 651,
"branch": "brockenhurst/hotfix/relationship-evidence-fk-race",
"buildLogTail": [
{ "timestamp": "...", "message": "[err] [builder 4/6] COPY package.json ./" },
{ "timestamp": "...", "message": "[err] Build Failed: ... \"/package.json\": not found" }
],
"buildErrorKinds": {
"Build Failed: failed to compute cache key: ... not found": 1
},
"buildLineCount": 42
},
"errors": [
{ "timestamp": "...", "level": "error", "message": "...", "module": "..." }
],
"warnings": [ ... ],
"errorTotal": 847,
"warningTotal": 12,
"errorKinds": {
"insert or update on relationship_evidence row violates FK": 847
},
"warningKinds": { "slow query took ms": 12 },
"truncated": true,
"envVarNames": ["ALLOWED_ORIGINS", "DATABASE_URL", "OPENAI_API_KEY", ...]
},
{ "name": "Redis", ... },
{ "name": "loamdb-postgres", ... }
]
}
filterisnullwhen no--servicefilter was applied, otherwise{"service": ""}.summaryis a roll-up across the (possibly filtered)servicesarray:servicescount,failures(services whose latest deploy is notSUCCESSand not stopped), plus pre-cap totals oferrorsandwarnings(so a flood of identical errors is reflected here even though each service'serrors[]is truncated).errorTotal/warningTotalon each service snapshot are pre-cap counts.errors[]andwarnings[]remain capped and deduped for readability; always compare the two to tell whether truncation is hiding something.errorKinds/warningKindsare fingerprint-bucketed counts (top 10 by frequency). UUIDs, numbers, hex, and quoted strings are normalised away before bucketing, so "FK violation on row 12345" and "FK violation on row 67890" collapse into one bucket. Read these before trusting the individual lines — a truncatederrors[]of 20 can misrepresent 800 identical failures.truncatedistruewhen the pre-cap total exceeds what's shown inerrors[]/warnings[].- Field names are stable across releases; changes are additive. Prefer
jqagainst these keys rather than parsing human output.
Log classification
- Pulls last ~500 lines per service via
railway logs --json --since. - Postgres-aware classification runs first: lines matching
UTC [] :are classified by the embeddedLEVEL(pg'sLOG,STATEMENT,DETAIL,HINT,NOTICE→ ignored;ERROR,FATAL,PANIC→ error;WARNING→ warning). Without this, Railway's "all stderr is level=error" envelope buries the real signal under hundreds of routineLOG: checkpoint completelines. - Then classifies by the
levelfield (Pino/stdlib logger convention), falling back to regex on themessagetext (/\b(error|fatal|panic|exception|traceback|unhandled)\b/ifor errors,/\b(warn|warning)\b/ifor warnings). - Dedupes consecutive identical messages. Caps each bucket at
--limit(default 20) most-recent entries per service. Theerrorssubcommand uses its own--limit(default 50) for deeper single-service triage. - Truncation is visible:
errors[]/warnings[]are capped, buterrorTotal/warningTotal/errorKinds/warningKindsare computed across every classified line, so you can always tell how much the cap is hiding.
Architecture note
Env var values never touch stdout. The only code path that reads them is strip_env_values() which runs json.loads on the CLI output, extracts the keys, and explicitly dels the parsed dict. There is no branch anywhere in the program that emits a value. The unit tests pin this invariant — test_strip_env_values_leaks_no_substring asserts that the concatenated stdout output of the builder contains zero characters from any input value.
Parallelism: per-service log + variable fetches are fanned out across a thread pool (max 8 workers). Total wall time for the user's prod environment (5 services) is typically ` when:**
- Running
build-logswith a large--linesvalue (200+). - Running
overviewof an env with many services, especially whenlatestDeployis FAILED (the auto-attachedbuildLogTailadds another ~30 lines per failed service). - Running
errorswith high--limitduring a log flood.
What happens: the full JSON payload is written to the file you specified. Stdout returns only a compact envelope:
{
"tool": {"name": "railway-ops", "version": "..."},
"payloadPath": "/abs/path/to/file.json",
"bytes": 12453,
"fileLineCount": 418,
"payloadKeys": ["project", "env", "summary", "services"],
"payloadShape": {
"project": {"type": "string", "length": 12},
"services": {"type": "list", "length": 5,
"sample": {"type": "dict", "keys": 8,
"shape": {"name": {"type": "string", "length": 6},
"errors": {"type": "list", "length": 23},
"latestDeploy": {"type": "dict", "keys": 6}}}}
},
"preview": { "head": [...], "tail": [...] } // only for log-shaped payloads
}
How to act on the envelope:
- Look at
payloadShapefirst — it tells you which top-level key has the data you care about and how deep to go. - Use the
Readtool with offset/limit to pull only the slice you need —Read /tmp/railway.json --offset 0 --limit 50gives the top of the file; thefileLineCounttells you the upper bound. - If the payload is log-shaped (has a
lineskey), thepreview.headandpreview.tailin the envelope are often enough — you may not need toReadat all. - For list-shaped payloads (e.g.
projectscommand), the envelope haspayloadType: "list"+payloadLength+sampleShapedescribing the first item.
--shape-depth controls how deep payloadShape recurses. Default is 3 (two layers of nesting — enough for most nested structures). Drop to 1 if you only need top-level types and want a tiny envelope. Bump is capped at 3 to keep the envelope small.
Don't use --output when:
- The response is known to be small (
status,envs,services). The envelope overhead isn't worth it. - You need the data immediately in the same response to act on (e.g. parsing
errorKindsto decide the next step). The envelope doesn't include the data itself — it just points at the file.
Flags worth knowing
--pretty— indent JSON output (accepted on every subcommand).--output— offload full payload to disk (see section above).--shape-depth— depth ofpayloadShaperecursion in the--outputenvelope (default3).--env— target a specific Railway environment (production, staging, …). Defaults to the linked env when omitted.--since—30s/5m/2h/1d/1w/ ISO 8601. Only affects log fetches.- `--
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: osouthgate
- Source: osouthgate/agent-plus-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.