Hooks and safety
Git hooks
Hooks install automatically on pnpm install. Manage them with:
pnpm hooks:status
pnpm hooks:disable-allBypass a single commit with git commit --no-verify. Set GIT_HOOKS=0 to skip
hook setup entirely (CI/Docker).
Where each gate runs
The checks are six independent triggers, not a pipeline: no CI job declares needs, the pull-request-only jobs run in parallel with validate, and the docs deployment and live-provider workflows are separate.
Mermaid updated 2026-11-01
Scope of each trigger:
- These are independent triggers, not a pipeline. No job declares
needs:in the Validate workflow, so its PR-onlystaticjob runs in parallel withvalidate. The static checks share one checkout, toolchain setup and dependency install; named steps report each failure and continue independent checks. - The
validatejob runs for ready PRs and pushes tomain. Draft PRs skip both Validate jobs; marking a PR ready, or changing the base branch of a ready PR, schedules them without needing another commit. Title/body-only edits skip them. - The pre-push hook runs validate, build:check, type-check and the two gates, and deliberately skips the test suite and smoke to stay fast.
oat status --scope project --hookin pre-commit is non-blocking.- Docs CI is PR-only and path-scoped.
- Deploy Docs is an independent workflow, also dispatchable on top of its push-to-
maintrigger. Live Provider E2E is the dispatch-only workflow, and nothing invokes it. - The Release workflow fires on
consensus-v*/session-v*tags. It builds, asserts generated outputs are committed, reruns the static suite, and verifies the tag against the already-written manifests.
Pre-commit: lint-staged
The pre-commit hook runs lint-staged over staged files only. JavaScript,
TypeScript, JSON, and Markdown are formatted with oxfmt; JavaScript and
TypeScript are also linted with oxlint. Config: .oxlintrc.json,
.oxfmtrc.json, and .lintstagedrc.mjs.
Adoption is incremental: the pre-commit hook runs lint-staged over staged
files only, and CI lints/format-checks only the files a PR changes. The repo has
not yet been formatted wholesale; a one-time repo-wide oxfmt is a planned
follow-up. Until then, do not run pnpm format across the whole tree in unrelated
PRs.
Pre-push: skill version-bump enforcement
The pre-push hook runs validate, build:check, type-check, skill-version
validation, and internal-flag validation. Tests and smoke are intentionally
left to the full local check set and CI, not duplicated in this fast hook.
Changed skills must bump their version. Any change under a canonical skill
directory (src/skills/<name>/) requires that skill's quoted
metadata.version to increase. This is enforced by
scripts/validate-skill-versions.ts:
pnpm run validate:skill-versions -- --base-ref <ref>It is wired into the PR-only static CI job and the local pre-push
hook. Shared roots, generated outputs, plugin-shared areas, and rename history
can affect more than the directly edited path; see the
version-impact table.
The same check also requires a changelog entry: for every canonical skill or
plugin release whose version changes, CHANGELOG.md must gain a new line inside
its ## [Unreleased] section that names that skill or plugin together with its
new version, so a version bump can never ship without release notes and an
unrelated entry cannot stand in for the missing one. A release PR that moves the
Unreleased entries under a new ## [x.y.z] - date heading satisfies the check
for the plugin at that version through the heading itself.
Pre-push: OAT tooling internal-flag enforcement
The OAT tooling skills mirrored under .agents/skills/** must stay hidden from
npx skills discovery — the CLI honors metadata.internal: true in frontmatter
to drop a skill from normal discovery (it reappears only under
INSTALL_INTERNAL_SKILLS=1). Because those files are regenerated by
oat tools update / oat sync, the flag is re-applied by an idempotent script and
guarded by a gate rather than hand-edited:
-
After refreshing tooling, re-stamp the flag:
pnpm exec tsx scripts/apply-internal-flags.ts, thenoat sync. The script is idempotent and skips the symlinkedsession-observermirror (a canonical standalone skill that must stay publicly discoverable). -
The detector
scripts/validate-internal-flags.ts:pnpm run validate:internal-flagsis wired into the PR-only
staticCI job and the localpre-pushhook, so a missing flag cannot merge tomain. If it fails, run the apply script and re-commit the stamped files.
Do not add this flag to canonical or generated product skill payloads under
src/skills/, skills/, or plugins/*/skills/; those are intended public
entries. See DR-260627 for the rationale.
Lint/format exclusions
Never lint/format generated, OAT-synced, or agent-instruction files: generated
installation outputs from scripts/build-generated.ts, .agents/**,
.claude/rules/**, .cursor/rules/**, and AGENTS.md at every level.
Format exclusions must stay in sync across .oxfmtrc.json, .lintstagedrc.mjs,
and the CI oxfmt --check step in .github/workflows/validate.yml; generated
.mjs lint exclusions must also stay in sync across .oxlintrc.json,
.lintstagedrc.mjs, and the CI oxlint step.
Because JSON config files cannot import TypeScript, the two ignorePatterns
lists cannot read the build's distribution catalog the way .lintstagedrc.mjs
does. They instead use collapsed globs: every directory under skills/ and
plugins/*/skills/ is a generated installation or provider view, so skills/**
and plugins/*/skills/** cover them all, and the two generated
plugins/consensus/scripts/*.mjs bundles are listed by path. Only src/skills/
is authored.
The covers generated roots in static lint and format configs case in
tests/tooling/generated-output-sync.test.ts guards the collapse by matching
semantics rather than literal listing: every generated output root must be
matched by some pattern in both configs, and those patterns must not match
authored paths such as src/skills/**. A new generated root that escapes the
globs and an over-broad glob that swallows authored source both fail the test.
A single shared ignore file passed to both tools with --ignore-path is not an
option: oxlint ignores the leading-slash anchor in such a file, so a /skills/
entry would also exclude authored src/skills/ and .claude/skills/.
oxlint/oxfmt are dev tooling — they do not touch what shipped skills run.