Skip to content

Hooks & CI

Interception happens at four points, all thin wrappers around the same core checker.

Terminal window
stylist hook install

Writes .git/hooks/pre-commit (honoring core.hooksPath and worktrees). On commit, every staged checkable file (HTML, Markdown, Word, PowerPoint, SVG, and raster images) is checked using its staged blob content: what will actually be committed, not the working tree. Blocking violations stop the commit (exit 1) with the violation list; advisory violations are printed for the committer but never block; clean commits stay silent.

If a pre-commit hook already exists, install refuses to overwrite it and prints the one line to add to your existing hook:

Terminal window
stylist hook pre-commit || exit 1

For hook managers (husky, lefthook), get the standalone script without touching the repo:

Terminal window
stylist hook install --print

If the repo has a .pre-commit-config.yaml, install refuses instead of writing .git/hooks/pre-commit. The pre-commit framework regenerates that file from its own config on every pre-commit install, so a hand-installed or hand-chained hook there would just get silently overwritten. Add stylist as a repo: entry instead, using the .pre-commit-hooks.yaml manifest this project ships:

- repo: https://github.com/lukedevops/stylist
rev: vX.Y.Z
hooks:
- id: stylist

Note that local git hooks are advisory: git commit --no-verify bypasses them. Treat the hook as fast feedback; CI is the backstop.

stylist ships a Claude Code PostToolUse adapter: whenever the agent writes or edits a checkable asset (HTML, Markdown, Word, PowerPoint, SVG, raster images), the file is checked, and blocking violations are fed back to the model (exit 2 → stderr), which then fixes its own output before the user ever sees it. Advisory-only verdicts let the write stand.

Get the settings.json snippet:

Terminal window
stylist hook claude-code --print-config
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [{ "type": "command", "command": "stylist hook claude-code" }]
}
]
}
}

For fleet rollout, push this via managed settings. Config discovery walks up from the written file’s directory, so rules follow the asset regardless of where the agent runs. Non-checkable writes and read-only tools exit 0 silently.

Coverage limit: the matcher only sees files written through the Write/Edit/MultiEdit tools. Assets an agent produces via Bash (cat > page.html, cp, image generators, curl -o) skip generation-time checking. Intercepting arbitrary shell commands reliably isn’t possible. This is why the layers behind it exist: the git pre-commit hook checks staged blobs no matter how they were produced, and CI mode walks the tree. An asset that dodges the agent-time check is still caught before it lands.

The same self-correction loop works with Codex CLI (v0.117.0+), whose hooks system mirrors Claude Code’s: PostToolUse events arrive as JSON on stdin, exit 2 blocks and feeds stderr back to the model. The difference is the payload shape: Codex reports file writes as one apply_patch call carrying the raw patch envelope, so a single event can create, update, or move several files. The adapter parses the envelope, resolves paths against the event’s cwd, and checks every checkable asset the patch touched; any blocking violation rejects the result with the full violation list (passing files stay silent, and deleted files are skipped, since there is nothing left to check).

Get the snippet for ~/.codex/hooks.json (or a repo-level .codex/hooks.json):

Terminal window
stylist hook codex --print-config
{
"hooks": {
"PostToolUse": [
{
"matcher": "^apply_patch$",
"hooks": [{ "type": "command", "command": "stylist hook codex" }]
}
]
}
}

Codex requires user review of non-managed hooks before they run (/hooks inside Codex to trust it). The Claude Code adapter’s coverage limit applies here identically: only apply_patch writes are seen; shell-produced assets rely on the pre-commit and CI layers.

Why no Cursor adapter: Cursor’s afterFileEdit hook is observational: it cannot block an edit or return feedback to the agent, so the self-correction loop above is impossible there today. Cursor users get coverage from the git pre-commit hook and CI mode; an adapter becomes worthwhile if Cursor makes post-edit hooks blocking.

Both the Claude Code and Codex hooks can also nudge the agent toward supplying context the hook itself has no way to know: an asset’s audience, or which named tone profile applies. They do this with the additionalContext field both hooks’ PostToolUse output supports ({"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"..."}} on stdout, exit 0). The agent reads it on its next turn and can act on it; it never blocks or forces a retry.

The nudge fires when:

  • The asset is loose (no .stylist.yaml) and its audience is unspecified. A loose asset has no project file to declare audience: in, so the only levers are -audience/STYLIST_AUDIENCE or the in-content marker below.
  • The ruleset declares tone.profiles[] and none resolved for this asset (no -tone-profile override, no path match). This applies to project assets too, since a .stylist.yaml can’t vary a value per-asset either.

A clean write gets the nudge as additionalContext; a violating write gets the same message appended to the stderr violation list (Codex folds every touched file’s nudge into one additionalContext call, since one apply_patch can span several files but stdout must still be a single JSON document).

stylist: welcome.html — its audience wasn't declared (add
<!-- stylist: audience=internal|external --> near the top of the file,
or pass -audience internal|external). If you know more, re-run
`stylist check [-audience internal|external] [-tone-profile <name>]
welcome.html` yourself for more accurate results.

In-content marker. For HTML, Markdown, and SVG assets (all support <!-- --> comments), an agent can declare both values directly in the file instead of re-invoking the CLI:

<!-- stylist: audience=external tone-profile=email -->

The marker is read by both the Claude Code and Codex hooks (not by stylist check’s batch/CI path, which resolves one Source for a whole multi-file run rather than per asset), and only fills in values nothing stronger already set: -audience/STYLIST_AUDIENCE/.stylist.yaml always win over it for audience. An unrecognized value is dropped, not applied.

.pptx and .docx files can’t carry a text comment (they are zip packages, not plain text), so they use a different mechanism: a stylist:audience/stylist:tone-profile custom document property stored in docProps/custom.xml, the same metadata part both formats already have. This is set with whatever tool wrote the property (for example python-docx/python-pptx), not by hand-editing the file. It is read the same way as the text comment and follows the same precedence rules. Raster images have no metadata slot at all, so their nudge text omits the marker option.

This is best-effort, not enforcement: nothing stops an agent from ignoring the nudge or never writing the marker. The git pre-commit hook and CI mode remain the backstop regardless of whether audience/tone guidance was ever declared.

stylist check accepts multiple files and directories:

Terminal window
stylist check docs/ landing.html # exit 1 if any asset fails

Directories are walked for checkable assets — HTML, Markdown, Word, PowerPoint, SVG, and raster images (skipping dot-directories, node_modules, vendor). -json emits an array of verdicts for multiple assets (a single asset stays a JSON object for backward compatibility). Exit 1 requires a blocking violation; advisory-only results exit 0, so tracking outdated assets in CI never turns builds red.

By default, checks made against the core API (api_url mode) are only attributed to the org’s shared token, not to the person who ran them. stylist login adds a person’s own identity to every check they submit afterward, so the dashboard’s “By person” breakdown can show who generated what:

Terminal window
stylist login -oidc-issuer https://your-idp.example \
-oidc-cli-client-id stylist-cli

This runs an OIDC device-code flow: the command prints a URL and a code, you approve it in a browser, and stylist stores the resulting credential (refreshable, so you don’t need to log in again for a long time) under your home directory. Every check made after that carries your verified email alongside the org token, both locally and from CI if the credential is present there too. stylist logout deletes the stored credential; checks then fall back to the org token alone, same as before you ever logged in.

The server only accepts personal tokens if it was started with -oidc-cli-client-id. See Server. Login failure (expired credential, unreachable IdP) never blocks a check; it just falls back to the org token, so a login problem is never a fail-open concern.

All hooks follow fail-open: style violations always block, but infrastructure failures (unreachable API, missing ruleset) warn and allow unless fail_open: false.