Skip to content

Hooks

Exohook runs the project’s validation checks at the right moments (before a commit, before a push, and in CI) from a single declarative source. The configuration lives in .config/exo/hooks.toml. Git hooks and the GitHub Actions workflow are both projections of that file, not independent copies that can drift (RFC 0137).

A hooks.toml defines two things: checks and workflows. A check is a single command with a scope filter. A workflow is an ordered list of checks that run together against a defined scope of changes.

The repository’s current workflows:

WorkflowScopeRunsPurpose
devuncommittedformat, lint, type-check, check, TOML and link verificationFast feedback on working-tree changes
coherencestagedthe same fast checksPre-commit gate
gatecommitted-not-pushedtestPre-push gate
ciHEADtest, coverageAuthoritative check on the pushed commit

coherence and gate are bound to git hooks:

[hooks]
pre_commit = "coherence"
pre_push = "gate"

The dev workflow has no scope of committed changes; it operates on the uncommitted working tree, so an agent can run it while editing without staging first.

A check declares a category. Most checks are pure verifications: they read state and report pass or fail. A check marked category = "mutate" is allowed to rewrite files (for example, rust-fmt reformats source, and rust-clippy ships a fix_command). The distinction matters because mutating checks change the working tree as a side effect of running; verifying checks never do.

Checks can be filtered to the files they care about. rust-fmt only runs on **/*.rs; verify-exo-links only runs on **/*.md. A check whose filter matches none of the changed files is skipped.

The same lane definitions drive both local enforcement and CI:

ProjectionOutput
Git hooks.git/hooks/pre-commit, .git/hooks/pre-push
GitHub Actions.github/workflows/exo-ci.yml

The CI workflow is generated from the ci lane (RFC 0137):

Terminal window
exohook ci emit github-actions

The generator emits one GitHub Actions job per check in the lane, so CI runs exactly what the local ci lane runs. Each job sets up only the toolchain its command needs; jobs run in parallel. The generated file carries an # AUTO-GENERATED by exohook header. Exohook regenerates any file carrying that header after a successful command, and leaves files without it untouched. Removing the header opts a file out of regeneration. To disable the projection entirely, set enabled = false under [projections.github_actions].

Because the workflow is generated, it is the kind of file that re-emits on every run. Treat .github/workflows/exo-ci.yml as derived: regenerate it through exohook rather than editing it by hand, and do not commit incidental regeneration drift.

Long-running checks such as builds and tests can appear to hang when they produce no visible output, leading to premature abandonment. Exohook renders a rolling terminal viewport for each running check: the last few lines as they would appear in a real terminal, including in-place progress bars driven by carriage returns. Output is parsed through a VTE state machine so escape sequences, carriage returns, and line feeds are handled the way a terminal would handle them. The viewport shows what you would see right now, not just completed lines, so a check that is working but quiet still shows motion.