Open source
Run checks locally
Detect your project's checks, write a .hookci.yml, validate it and run the same jobs on your own machine, with no hub and no account.
The hookci binary works on its own. detect, init, validate and run need no hub, no Slack and no GitHub App, so you can write and debug a config before any CI exists. The examples below use a small Node app with lint, typecheck and test scripts in package.json and a package-lock.json.
See what HookCI detects
hookci detect reads the project and prints the config it would use. It writes nothing to disk.
~/app recording $ hookci detect # package manager: npm (package-lock.json) version: 1 checks: setup: - run: npm ci cache: key: [package-lock.json] paths: [node_modules] jobs: lint: run: npm run lint annotate: eslint test: run: npm run test typecheck: run: npm run typecheck annotate: tsc
Illustration. Detection from package.json scripts and the lockfile.
Notes about the project, such as which lockfile decided the package manager, come out as a comment at the top. Detection covers Node (npm, pnpm, yarn, bun, and Turborepo tasks), Go, Rust and Python, and adds services when it finds a Docker Compose file. Use --json for machine-readable output.
If a .hookci.yml already exists, detect prints note: .hookci.yml exists and takes precedence over detection to stderr. If it finds nothing, it exits with code 3.
Write a config
hookci init saves the detected config as .hookci.yml in the project root, then you edit it.
~/app recording $ hookci init wrote /home/dev/app/.hookci.yml $ hookci init /home/dev/app/.hookci.yml already exists (use --force to overwrite)
Illustration. init refuses to overwrite an existing file unless you pass --force.
When nothing is detected, init writes a skeleton whose test job is echo "edit .hookci.yml and set the test command" && exit 1, so a forgotten placeholder fails loudly instead of passing. The full set of keys is in the config reference, and config examples shows common shapes.
Validate it
hookci validate checks the file without running anything. It defaults to .hookci.yml in the current directory.
~/app recording $ hookci validate ok: .hookci.yml (3 jobs) $ echo $? 0
Illustration. Errors go to stderr with a non-zero exit code.
Use --json to get diagnostics as a JSON array, which is handy in a pre-commit hook. Pass --self-hosted when the config sets trust: all, which is only allowed on a deployment you operate. Exit code 2 means the config has errors, and 4 means the file could not be read at all. See Trust and forks for what trust does.
Run the jobs
hookci run executes the jobs of the commit at HEAD. It does not read your working tree, so commit first. Without arguments it runs every job. With a job name it runs that job and the jobs it needs, in plan order.
Jobs run in an isolated systemd unit as the hookci-run user, which is what makes run match what a paired agent does. That needs root and the installed units from Install the agent:
~/app recording $ sudo hookci run [14:02:11] run 01JD3W8K2Q7ZP4N6V9T5X0BHRC running [14:02:11] lint running [14:02:11] test running [14:02:11] typecheck running [14:02:19] lint success 4.2s [14:02:23] typecheck success 8.1s [14:02:38] test failed 19.4s | FAIL src/cart.test.ts | expected 3, received 2 reason: exit 1 [14:02:38] run 01JD3W8K2Q7ZP4N6V9T5X0BHRC failed (3 jobs, 27.3s) hookci: run failed: job test failed $ echo $? 1
Illustration. Progress lines are timestamped. A failing job prints the tail of its log.
The job names, durations and run ID above are illustrative. Add -v before the command for step-by-step events and mirrored job output:
sudo hookci -v run
sudo hookci run test
Run one job to iterate on a failure quickly. An unknown name lists the valid ones and exits with code 2.
Without root
On a laptop, or anywhere you cannot install the units, turn the sandbox off:
hookci --isolation none run
HookCI prints a warning that jobs then run as your own user, with no sandbox, no resource limits and full network access. Use this only on code you trust. The global flags go before the command, never after it.
Where results go
Each run writes to a state directory: $HOOKCI_STATE_DIR if set, else $XDG_STATE_HOME/hookci, else ~/.local/state/hookci. Override it with --state-dir DIR. Note that sudo may change HOME, so a run under sudo can land in root’s directory. Pass the same --state-dir to every command to keep them together.
hookci status
hookci status 01JD3W8K2Q7ZP4N6V9T5X0BHRC
hookci logs 01JD3W8K2Q7ZP4N6V9T5X0BHRC test
status lists the last 20 runs, or the jobs of one run. logs prints the setup log and each job’s log under a ==> name <== header. Logs are plain files at <state-dir>/runs/<run>/logs/<job>.log. Only one hookci process can use a state directory at a time.
Press Ctrl-C to cancel: the run stops with the reason interrupted by signal.
Exit codes
| Code | When |
|---|---|
| 0 | The run succeeded. |
| 1 | The run failed or was cancelled. |
| 2 | Not a git repository, invalid config, unknown job or bad flags. |
| 3 | No checks found. Add a .hookci.yml or run hookci init. |
| 4 | Internal error. |
The same codes apply to detect and validate where they make sense. The full list is on the CLI reference. When the local run passes, push the config and let a paired agent run it: Quickstart.