Hooks & CI
Interception happens at four points, all thin wrappers around the same core checker.
git pre-commit hook
Section titled “git pre-commit hook”stylist hook installWrites .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:
stylist hook pre-commit || exit 1For hook managers (husky, lefthook), get the standalone script without touching the repo:
stylist hook install --printIf 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: stylistNote that local git hooks are advisory: git commit --no-verify
bypasses them. Treat the hook as fast feedback; CI is the backstop.
Claude Code hook
Section titled “Claude Code hook”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:
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.
Codex hook
Section titled “Codex hook”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):
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.
Context nudges (audience & tone profile)
Section titled “Context nudges (audience & tone profile)”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 declareaudience:in, so the only levers are-audience/STYLIST_AUDIENCEor the in-content marker below. - The ruleset declares
tone.profiles[]and none resolved for this asset (no-tone-profileoverride, no pathmatch). This applies to project assets too, since a.stylist.yamlcan’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.
CI mode
Section titled “CI mode”stylist check accepts multiple files and directories:
stylist check docs/ landing.html # exit 1 if any asset failsDirectories 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.
Personal attribution (stylist login)
Section titled “Personal attribution (stylist login)”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:
stylist login -oidc-issuer https://your-idp.example \ -oidc-cli-client-id stylist-cliThis 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.
Failure behavior
Section titled “Failure behavior”All hooks follow fail-open: style
violations always block, but infrastructure failures (unreachable API,
missing ruleset) warn and allow unless fail_open: false.