Hook Runtime Bootstrap Model
Decision
The consumer repository hook shim must not own policy behavior.
Its job is limited to:
- discover the consumer repository root
- locate the checked-out
coding-ethosbundle for that repository - verify that required built artifacts exist inside that checkout
- repair missing artifacts by running supported
maketargets in thecoding-ethoscheckout - dispatch to the built hook binary
Policy evaluation, policy freshness, generated prompt packs, runtime command
selection, managed capture, diagnostics parsing, and hook behavior belong to
the coding-ethos checkout. The shim is only a bootstrap and dispatch layer.
Rationale
The coding-ethos checkout is already required for normal operation. Keeping a
second runtime cache under the consumer repository .git directory creates two
possible sources of truth:
- the checked-out
coding-ethossource tree - the installed
.git/coding-ethos-hooksruntime cache
That split is fragile. Worktrees, submodules, generated policy files, touched
configuration, and branch switches can cause the hook shim, make build, and
runtime validation to resolve different roots. When that happens, lifecycle
hooks can fail even though the correct repair command has been run elsewhere.
The simpler invariant is:
make in the coding-ethos checkout builds the hook runtime.
hooks run the hook runtime from the coding-ethos checkout.
If the coding-ethos checkout is present and can build, the hook path should
self-heal missing runtime artifacts. If the checkout is missing or cannot build,
the error should name the exact checkout path and command required to fix it.
Target Runtime Layout
Runtime artifacts live under the coding-ethos checkout and are ignored by
Git:
coding-ethos/
bin/
coding-ethos-git-hook
coding-ethos-policy
coding-ethos-lint
coding-ethos-agent-hooks
build/
policy/
policy-bundle.json
policy-metadata.json
gemini/
prompt-pack.json
toolchain/
manifest.tsv
go-bin/{golangci-lint,shfmt}
github-bin/{actionlint,dotenv-linter,hadolint,shellcheck}
prefix/bin/
Consumer repository hooks should not install or validate policy bundles under
the consumer .git directory.
Managed Toolchain
Hook execution must not depend on host linters or formatters being installed on
PATH. The same checkout-local runtime model applies to third-party tools:
build/toolchain/go-bin/contains source-installed tools copied into a managed executable directory, starting withshfmtandgolangci-lint.build/toolchain/github-bin/contains binaries installed from pinned GitHub release assets, including ShellCheck, actionlint, hadolint, and dotenv-linter.build/toolchain/prefix/is the controlled prefix for source-installed tools that need a Unix-style install root.
make build is responsible for ensuring required managed tools exist before
hook execution. pre-commit/hooks/managed-toolchain.tsv is the checked-in
source manifest for required tool versions, release assets, and SHA-256
digests. make managed-toolchain-install installs those tools and writes
build/toolchain/manifest.tsv with the installed paths.
coding-ethos-run prepends the managed tool directories to PATH before
dispatching to the Go hook runtime. The Go hook runtime also resolves binary
tool commands to checkout-local managed paths when possible, so shfmt,
shellcheck, actionlint, hadolint, dotenv-linter, and golangci-lint do not silently fall
back to host binaries. Missing required managed tools or a missing installed
manifest are runtime artifact failures and should self-repair through the same
bootstrap path as missing policy binaries.
The managed toolchain has two installer surfaces:
- a GitHub release binary installer for pinned release assets with mandatory
SHA-256 verification and optional
GITHUB_TOKENauthentication for GitHub API and asset requests - a source install wrapper that sets checkout-local destinations before running
a tool build, currently covering Go
go installand Rustcargo install
Direct host installs such as go install ... into $HOME/go/bin are not a
runtime contract. They may unblock a local shell, but hooks must only rely on
artifacts under the coding-ethos checkout.
Build Versus Test Boundary
make build is the explicit environment mutation target. It may regenerate
configs, install managed tools, refresh provider settings, install hook
entrypoints, compile policy bundles, compile Go runtime binaries, and sync
parent hook runtime artifacts.
Test and diagnostic targets must not do those things implicitly. They consume
the artifacts produced by make build and fail fast when required artifacts are
missing. This prevents ordinary verification commands from rewriting a parent
worktree, reinstalling hooks, changing generated config, or performing hidden
build setup.
Go tests use the normal Go workflow. make go-test runs go test through
managed capture, and make go-e2e-test runs the e2e package with go test.
That preserves normalized diagnostics, CEL promotion, trace retention, and
SARIF-compatible output without introducing a separate compile-and-run test
path.
Hook Entrypoint Contract
The installed consumer repository hook entrypoint should be a small executable
script generated from the compiled bin/coding-ethos-run binary. The script
passes the hook kind and hook name explicitly, for example
coding-ethos-run git-hook pre-commit "$@", so installed Git hooks do not rely
on argv[0] inference.
The compiled runner owns the bootstrap contract:
- Resolve the consumer repository root from Git.
- Locate
coding-ethos, preferably at$consumer_root/coding-ethos. - Fail with a clear submodule checkout instruction if it is missing.
- Check for required artifacts in the
coding-ethoscheckout. -
If artifacts are missing, run:
make -C "$coding_ethos_root" build - Re-check the required artifacts.
- Exec the built hook binary from the
coding-ethoscheckout.
The hook entrypoint contract must not:
- compare source mtimes to compiled policy during lifecycle hook execution
- compile policy directly
- select policy files
- inspect policy source configuration
- rewrite generated protected files by hand
- maintain a second runtime cache in the consumer
.gitdirectory - write response caches or other transient runtime state into
.git
Repair Rules
Bootstrap repair should run only when required artifacts are missing or invalid enough that they cannot be executed. It should not run because a timestamp looks old.
Examples that should trigger repair:
- missing hook binary
- non-executable hook binary
- missing compiled policy bundle
- unreadable compiled policy bundle
- missing managed toolchain manifest
- missing required managed tool, such as
build/toolchain/go-bin/shfmt,build/toolchain/github-bin/dotenv-linter, orbuild/toolchain/github-bin/shellcheck
Examples that should not block lifecycle hooks:
- source config has a newer mtime than the compiled bundle
- generated files were touched by checkout tools
- another worktree has a different runtime cache
Strict freshness validation belongs in explicit maintainer/CI commands such as
make validate, make cutover-verify, and CI. Freshness is based on the
source hashes recorded in build/policy/policy-metadata.json, not mtimes.
Safety Requirements
Bootstrap needs a few guardrails:
- Use a recursion guard such as
CODING_ETHOS_BOOTSTRAP=1while runningmake build. - Use an interprocess lock, preferably
flock, so concurrent hooks do not run multiple builds over the same output directory. - Print the exact failed command and preserve build output when repair fails.
- Keep build outputs under ignored
bin/andbuild/directories. - Keep transient repo-local runtime caches under ignored
.coding-ethos/cache/paths, not under.git. - Keep installed hook entrypoints as stable generated scripts and move
versioned behavior into the
coding-ethoscheckout.
Hook Execution Model
Hook execution follows a three-phase model designed for maximum parallelism while preserving correctness ordering.
Phase 1 — Format (Sequential Gate, Per-Language Parallel)
Formatters mutate files and must complete before linters run. Within the format phase, per-language formatter chains run concurrently:
- Python lane (sequential):
pyupgrade→ruff format→ruff autofix - Go lane (sequential):
golangci-lint-format(gofmt + golines)
The two lanes operate on disjoint file sets and run in parallel goroutines.
A cross-language text fixer (fixText) runs first and gates both lanes.
If any formatter fails or the format phase produces a non-zero exit, the hook stops immediately.
Phase 2 — Analysis Groups (Fully Parallel)
All non-AI analysis groups run concurrently as independent goroutines:
syntax,docker,workflow,shellpython-policy,python-quality,python-staticdocs,security,go
Within a group, commands run sequentially by default. Groups may declare a
ParallelAfter index to split their command list into:
- Sequential prefix — prerequisites that must pass before parallel work
- Parallel suffix — independent commands that run concurrently
For example, the go group uses ParallelAfter: 2:
| Index | Command | Phase |
|---|---|---|
| 0 | go-format |
Sequential |
| 1 | go-vet |
Sequential |
| 2 | go-test |
Parallel |
| 3 | go-coverage |
Parallel |
| 4 | golangci-lint |
Parallel |
If any sequential prefix command fails, the parallel suffix is skipped for that group.
Phase 3 — AI (Gated)
AI review groups (e.g., gemini-check) run only after all Phase 2 groups
succeed. This avoids wasting API credits on code that has already failed
deterministic quality gates.
Incremental Linting
During pre-commit, golangci-lint receives --new-from-rev=HEAD so it only
reports issues in changed code. During pre-push, it runs on all files for
complete coverage.
Migration Direction
Runtime artifacts are built and executed from the checked-out coding-ethos
repository. New hook behavior must use that single source of truth instead of
adding cache-local compatibility paths.