Contributing
July 12, 2026 · View on GitHub
Thanks for looking at sshepherd. Issues and PRs are welcome.
Setup
git clone https://github.com/Antheurus/sshepherd.git
cd sshepherd
just install
Before opening a PR
just check # typecheck + lint (tsc --noEmit, biome check)
just test # bun test — unit tests, no live ssh required
just build # compiles dist/sshepherd
CI runs the same typecheck, lint, test, and build steps on every push and PR
(.github/workflows/ci.yml) and must pass. Do not commit dist/ — it's gitignored; release
binaries ship via GitHub Releases only. Add an
entry to docs/progress.md describing what changed and why (and docs/changelog.md too, if
the change is user-facing) — see the note at the bottom of this file.
just smoke runs scripts/smoke.sh against a disposable local sshd + Postgres Docker
fixture (scripts/sshd-fixture/). It isn't part of CI (no Docker host to drive in a GitHub
runner), but if your change touches a remote command's output shape — a new parser, a new
db query, a new check/services/logs op — run it locally before submitting. It's the
closest thing this project has to a regression suite for real Linux/Postgres behavior, since
bun test mocks the transport layer entirely.
Where things live
src/registry.ts— every op as data: group, action, args, whether it mutates, how its remote command is built, how its output is parsed and shaped. Adding an op is one array entry here, dispatched throughexecuteOp— never a new bespoke execution path or an if/elif chain in the CLI.src/transport.ts— the zero-knowledge core. Every op reaches the network throughtransport.run(), the single execution path; nothing spawnssshdirectly outside this file. It owns the ControlMaster socket lifecycle, the error classification enum, and discarding ssh's own stderr before it can ever leak into anErrorInfo.src/output.ts— the one place anEnvelope<T>is constructed (buildEnvelope), plus the--prettyrenderer.src/db.ts/src/targets.ts— thedbgroup's read-only enforcement (SQL parsing, the read-only transaction wrapper, pg-target resolution).src/recipes.ts— thedeploygroup's TOML recipe loader, step ordering, and dry-run planner.src/quote.ts—shq/shellJoin, the only sanctioned way to interpolate a value into a remote command string.src/cli.ts— argv parsing and group/action dispatch, generated from the registry (help text is registry-derived, never a second hand-written list).src/__tests__/— one test file per module,bun testrunner, an injectable fake ssh runner (no real network calls).
The zero-knowledge rule contributors must never break
- No host, user, port, or IP ever reaches a response, a log line, or an error message.
Envelope<T>has no such field by design — don't add one, and don't smuggle host/user data intodataon a new op. - Every remote command runs through
transport.run(). Never spawnssh/scpdirectly from an op, a parser, or the CLI. - Every value interpolated into a remote command string goes through
shq/shellJoin(src/quote.ts). Never string-concatenate an unquoted value into a command. - A mutating op (
mutating: truein its registry entry) must go through the singleconfirmGate+auditMutatingpath inexecuteOp— don't add a second, parallel confirmation mechanism for a new op. db query's free-text SQL path must keep going throughassertSelectOnly+assertNoMultiStatementSql+ the read-only transaction wrapper. If you're touchingsrc/db.ts, readreferences/db.mdfirst — the three enforcement layers and why each one exists are documented there.
Style
No new abstractions or refactors bundled into a feature/fix PR — keep changes scoped to what
they're fixing. Match the existing patterns (data-driven registry, one transport path, one
envelope constructor) rather than introducing a parallel approach for the same problem.
biome check enforces formatting — run just check before pushing, or bun run lint:fix to
auto-fix.
Docs
If a change is user-facing, update docs/changelog.md. Either way, add an entry to
docs/progress.md describing what changed and why — it's the project's own history log,
read by whoever (human or agent) picks up the next session.