Local CI for AI Coding Agents
Highball runs your repo's own checks after every agent edit, and the full checks when the turn ends. A failure blocks the agent and hands it the output. One YAML file. No account, no server.

$ npx @profoundry-us/highball run → Lint & formatting ... passed (2.1s) → lib/ types hold ... FAILED (4.8s) → Coverage never decreases ... todo (not implemented yet) reported 3 events to us.i.posthog.com ### lib/ types hold (lib-types) failed. Fix before finishing: lib/locations.js(248,3): error TS2339: Property 'moved' does not exist on type '{ id: string; root: string; ... }'. exit 2 — the agent gets this back as its next instruction
Cheap after every edit. Everything at turn end. Loud when it fails.
Fast checks are the ones you'd happily run a hundred times a session. The full checks are the ones that decide whether the turn is over.

Every check here is a file-scoped script or a grep. They fire after each Write, Edit and Bash call, and --if-changed keeps the misses free.

The suite, two AI-judged rubrics, and a todo check that's tracked but can't fail. Any check opens to the exact command and its captured output.
When one fails, the agent is told exactly which, and why
Failed checks open on their own, command and output first. The same text goes back to the agent on stderr with exit 2 — it doesn't get to finish the turn until both are green.
### The blueprint lints (blueprint-lint) failed. Fix before finishing: ERRORS ✗ [coverage] status.spec.steps-are-required: a check references this rule id, but no such rule exists ✗ 137 rules, 7 screens, 57 anchors — 1 error(s), 6 warning(s) ### Unit tests (unit-tests) failed. Fix before finishing: tests 316 · pass 315 · fail 1 ✖ failing tests: test/delivery.test.js:80:1

Enforcement is local. Everything else is optional.
Two hooks, one YAML file, and an exit code Claude Code already respects. Nothing here needs a login.
// .claude/settings.json "PostToolUse": [{ "matcher": "Write|Edit|Bash", "command": "npx @profoundry-us/highball run --fast --if-changed" }]
Fast checks on every edit
Checks marked fast: true run after every Write, Edit — and Bash, because agents in auto mode edit through the shell.
"Stop": [{ "command": "npx @profoundry-us/highball run", "timeout": 900 }] → 12 checks · 41.6s · passed
Full checks at turn end
The Stop hook runs every check in the file. The turn can't end until all of them pass.
### Unit tests (unit-tests) failed. Fix before finishing: Failures: 1) journal keeps every rule's output expected 3, got 2 $? 2
Blocking exit codes
Exit 2 is the Claude Code hook contract: it stops the agent and hands stderr back as its next instruction. No dashboard to ignore.
$ highball run --fast --if-changed highball: no changes since the last run — skipped fingerprint HEAD 8981d5b + dirty paths (size, mtime) stamp ~/.highball/stamps/webtree-3f9a2c cost 11ms · no checks spawned
Change detection
--if-changed fingerprints the working tree and exits in milliseconds when nothing has moved. That is what keeps a Bash hook free: it fires after every command, and most commands are reads. Per checkout, so worktrees never share a skip.
.highball/disabled this checkout gitignored next run enabled: false the whole team committed next run HIGHBALL_DISABLED=1 this machine env next session highball: disabled by .highball/disabled (bisecting a flaky test) — no checks run
Three off switches
This checkout, the whole team, or this machine. Each one announces itself on every run, with the reason you left in the file — a guardrail that stopped guarding should never read green.
# .highball/checks.yml - id: no-new-todos name: No new TODOs in the diff rubric: .highball/rubrics/no-new-todos.md judged by claude · turn end only
Rubric-based AI judgements
A check can be a markdown rubric instead of a command. Claude judges the diff against it at turn end — never on the fast path, where latency and cost would be paid on every edit.
exec: via: docker compose exec -T app - id: rspec run: bundle exec rspec - id: gitleaks run: gitleaks detect exec: host
Container-aware execution
Declare exec.via once and every check runs inside your container. Checks that belong on the host opt out with exec: host.
reported 3 events to us.i.posthog.com highball_run status, duration_ms, rules_run, failed_rules highball_check rule_id, status, summary never code · never logs · key is write-only
Telemetry via your own PostHog
Check ids, pass/fail, durations, and a one-line failure summary, sent to a PostHog project you own. Never code, never logs — and an unreachable endpoint never blocks a check.
Enforcement, then witness.
Enforcement runs on your machine and can stop the agent. Telemetry is optional and best-effort, sent only to a PostHog project you own.
Blocked, with the output in hand
When a check fails the agent doesn't get a red X to scroll past. It gets the failing output as its next instruction, and the turn can't end until the check passes.
{
"session_id": "8f2c1a…41ab",
"hook_event_name": "Stop",
"transcript_path": "~/.claude/projects/…/8f2c1a…41ab.jsonl",
"cwd": "/Users/you/src/webtree"
}lib/ types hold (lib-types) failed. Fix before finishing: lib/locations.js(248,3): error TS2339: Property 'moved' does not exist…
Adding moved to the location type, then re-running the checks before I finish.
Every run, recorded in your own PostHog
Telemetry is best-effort and goes to a PostHog project you own. If the endpoint is down, checks still run and still block — they just aren't recorded.
{
"event": "highball_run",
"properties": {
"project": "webtree",
"trigger": "stop",
"branch": "main",
"status": "failed",
"duration_ms": 14200,
"rules_run": ["lint", "lib-types", "coverage-ratchet"],
"failed_rules": ["lib-types"]
}
}Ask your agent to install and onboard today!
Highball is set up by the agent it will check. The onboarding guide walks it through the repo's real toolchain, writes checks that reflect what the repo already trusts, and proves that exit 2 actually blocks.
› Run npx @profoundry-us/highball onboard and follow the instructions.
