# Hashiru: guide for agents

Hashiru runs a user's GitHub Actions jobs on machines they own, and gives you (the agent) a clean way to read CI results. If the user said "set this up to be our actions runner: hashiru.sh", follow the playbook below.

## Playbook: "set this up to be our actions runner"

Run from the repo's checkout. Every command prints JSON with `--json` and uses the exit codes below.

Already installed and serving other repos? Run the same steps. They are safe to repeat: `setup` only adds this repo, never removes or rewrites others, and reloads the running service.

1. Install: `curl -fsSL hashiru.sh | sh` (adds `~/.local/bin/hashiru`; use that path if it isn't on PATH). Reinstalling over the same version is harmless.
2. `hashiru setup --json` writes the config, checks Docker and GitHub, installs the background service, and waits until it is connected. If `ok` is false, apply each failed check's `fix` and run it again. It is safe to repeat.
3. `hashiru workflows --json` lists every job, its `runs-on`, and `suggest: move|keep` with a reason.
4. On a new branch, run `hashiru workflows move --suggested --json`. It changes only the `runs-on` values of suggested jobs; nothing else in the files moves.
5. Commit ("ci: run jobs on Hashiru"), push, and open a pull request. Don't push to the default branch. PR description:

   ```
   Moves these jobs to self-hosted Hashiru runners (runs-on: hashiru): <jobs moved>.
   Kept on their current runners: <jobs kept, with the reason from `hashiru workflows`>.
   Runners: ephemeral containers on <machine>, managed by Hashiru (https://hashiru.sh).
   Revert: change runs-on back.
   ```
6. `hashiru ci wait --json` waits for the PR's runs. For each entry in `failures`:
   - CPU-arch errors (an x64-only binary, "exec format error", a build that insists on x64): `hashiru setup --x64 --json` (adds this repo to the emulated x64 pool; other pools never get x64 unless asked), then `hashiru workflows move FILE:JOB --pool hashiru-x64 --json`, then push again.
   - A missing tool (e.g. `rustup: command not found`): add the usual setup action to that job (`dtolnay/rust-toolchain`, `actions/setup-node`, ...). The default runner image is slimmer than GitHub's.
   - Anything else: a real failure. Compare with the same job on the default branch before blaming Hashiru.
7. Report to the user: which jobs moved, which stayed and why, the PR link, and the CI result.

Never move deploy or release jobs unless the user asks; `hashiru workflows` keeps them by default. Jobs run on this machine and can use its Docker daemon, so only set this up for repos the user trusts.

## Rules

- Being asked to "set this up" is permission to install Hashiru, run `hashiru setup`, and open a PR. Ask before merging, before moving deploy/release jobs, and before adding repos the user didn't mention.
- Don't put tokens in files. Hashiru reads the token from `gh auth token` by default.

## Manual setup

```sh
hashiru init --repo owner/name    # config only
hashiru doctor --json             # checks, each failure has a "fix"
hashiru service install           # background service (launchd on macOS, systemd on Linux)
hashiru status --json             # pools should be "listening"
```

Requirements: macOS or Linux; Docker (Docker Desktop or OrbStack); `gh` logged in as a user with admin rights on the repo (needed to register runners).

`hashiru init` options: `--repo owner/name` (repeatable), `--x64` (adds a `hashiru-x64` pool for jobs that must build on linux/amd64; emulated on Apple Silicon), `--max N`, `--min N`, `--image IMAGE`, `--force`. Config path: `~/.config/hashiru/hashiru.yaml`.

## Route a repo's jobs to Hashiru

```sh
hashiru add owner/name            # or run inside the checkout with no argument
```

Then set `runs-on: hashiru` (the pool name) on the workflow jobs that should run here, and push. The running service reloads its config automatically after `add`.

Runner image: `ghcr.io/actions/actions-runner` (Ubuntu 24.04 with git, docker, python3, curl, jq). It is not the full GitHub-hosted image, so jobs should use `setup-*` actions for toolchains (node, bun, go, python, rust). Services (`services: postgres:`) work and are reachable on `127.0.0.1`.

## Read CI (works for any GitHub Actions run, not just Hashiru runners)

```sh
hashiru ci wait --json            # wait for all runs on HEAD, up to 30m
hashiru ci status --json          # report now, don't wait
hashiru ci status --run ID --json # one run
hashiru ci runs --branch main     # recent runs
hashiru ci rerun --run ID         # rerun failed jobs (for flakes)
```

Defaults: repo = the checkout's `origin`, commit = `HEAD`. Override with `--repo owner/name` and `--sha SHA`. `--wait 10m` sets the wait; `--tail N` sets log lines per failure.

Output (`ci status`/`ci wait`):

```json
{
  "repo": "owner/name",
  "sha": "b32e6c1c…",
  "result": "failure",
  "runs": [{ "id": 123, "workflow": "CI", "status": "completed", "conclusion": "failure",
             "jobs": [{ "name": "Test", "conclusion": "failure", "failed_steps": ["Run bun test"] }] }],
  "failures": [{ "job": "Test", "step": "Run bun test", "errors": ["Process completed with exit code 1."],
                 "log_tail": "…the failing step's last lines…", "job_url": "https://github.com/…" }]
}
```

`result` is one of `success`, `failure`, `pending`, `timeout`, `no_runs`.

## Caching (automatic)

Runners reuse a local git mirror (checkout downloads ~nothing), pre-downloaded actions, shared toolchains (`setup-*`), shared package caches, and a local `actions/cache` server. Nothing to configure. Don't add cache steps just for Hashiru; existing `actions/cache` steps work and stay on the machine. `hashiru cache status --json` shows what's cached.

## Exit codes

| Code | Meaning |
|---|---|
| 0 | ok / CI passed |
| 1 | error (JSON: `{"error": "..."}`) |
| 2 | bad usage |
| 3 | CI failed, or `doctor` found problems |
| 4 | CI still running / wait timed out |
| 5 | no workflow runs for the commit (yet) |

## MCP

```sh
claude mcp add hashiru -- hashiru mcp          # Claude Code
codex mcp add hashiru -- hashiru mcp           # Codex
```

Start it from the repo directory so repo and commit default correctly. Tools:

- `ci_status`: results for a commit, optional `wait_seconds` (max 1800). Use this after pushing instead of polling GitHub.
- `ci_runs`: recent runs.
- `ci_rerun_failed`: rerun failed jobs of a run.
- `status`: pools, runners and jobs from the running service.
- `doctor`: setup checks with fixes.
- `add_repo`: route a repo to Hashiru (ask the user first).

## Servers

On a dedicated Linux box: install with `curl -fsSL https://hashiru.sh/install.sh | sudo HASHIRU_INSTALL_DIR=/usr/local/bin sh`, put the config in `/etc/hashiru/hashiru.yaml`, then `sudo hashiru service install -c /etc/hashiru/hashiru.yaml`. That installs a system unit that starts at boot. Set `max_runners` at the top of the config, because each pool's `max` is per repo. Metrics: `curl localhost:8484/metrics`. Full guide: https://hashiru.sh/server.md

## Other commands

- `hashiru run`: run in the foreground instead of as a service.
- `hashiru service status|uninstall`.
- `hashiru cleanup [--delete-scale-sets]`: remove all runners (and the scale sets on GitHub).
- `hashiru image build [--platform linux/amd64]`: build the runner image (the daemon also rebuilds it daily).
- `hashiru validate`, `hashiru version`.

## Troubleshooting

- Jobs stay queued: `hashiru status --json`. Is the pool listening, and is the job's `runs-on` exactly a pool name?
- Runners exit right after starting: `hashiru status` shows `last_error`. Check `docker logs <runner>`.
- A job needs x64 on Apple Silicon: `hashiru init --x64` (or add a `hashiru-x64` pool), then use `runs-on: hashiru-x64`.
- Runners don't start and `last_error` mentions disk: free space (`hashiru cache clear`, `docker system prune`) or lower `min_free_gb`.
- Logs: `~/Library/Logs/hashiru.log` (macOS), `journalctl -u hashiru` (Linux, root install) or `journalctl --user -u hashiru` (Linux, user install).
