0langa/stylelatch
Switchable output styles for Codex and Claude Code, enforced by hooks and switched from the chat with ::
Changelog
All notable changes to StyleLatch are recorded here.
The format follows Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
Added
-
A guard that reviews every style for language governing conduct rather than prose — skipping a confirmation, reporting a pass that did not happen, overriding the operator's instructions, relaxing a safety rule.
::testreports it, and latching such a style prints the same warning.It warns and never blocks: the model's own training is the real defence, and refusing on a regular expression would produce false confidence. It is tuned against false positives rather than for coverage, because a warning that fires on a reasonable style teaches people to ignore warnings. CI asserts that no style StyleLatch ships trips its own guard.
Fixed
- No hook ever ran under Claude Code on Windows. Claude Code runs a hook's
commandthrough Git Bash and does not readcommandWindows, which only Codex honours, and on Windowspython3is usually the Microsoft Store alias that prints Python was not found. The command now pickspy -3, orpython, on Windows and keepspython3everywhere else. - The
Stophook measured the wrong reply. It read only the transcript, which Claude Code documents as not guaranteed to hold the final message yet whenStopfires, and it took a tool call's input as prose, so aWriteof a Markdown file could be recorded as a breach. It now uses thelast_assistant_messagefield both hosts send, and the transcript fallback skips tool calls. ::test canary offwith no canary armed cleared the real global latch while reporting that nothing changed. It now only ever removes the canary.- Disarming the canary put back only the latch that had been winning. Arming stands down the session and project latches and takes over the global one, so the other two were lost, and re-arming while a more specific latch won replaced the snapshot and lost the original global latch too. Every latch it displaced now comes back, re-arm or not, unless a newer one was set while the canary was armed.
- The conduct guard never read a style's
nudge, which is the one part injected on every turn. It reads it now. - The conduct guard flagged styles that restate the boundary, such as never skip the confirmation, do not claim the tests passed unless you ran them or never hide errors, and matched inside words (chide the error). A negation sitting right on the phrase now clears it; one on another verb, as in don't hesitate to skip the confirmation, still does not.
- A style file saved as UTF-8 with a byte order mark, which is what Windows PowerShell 5.1 writes, lost its whole frontmatter: no nudge, no checks, and the frontmatter injected as part of the body. It parses now.
- With two sessions open, a breach in one was corrected in the other. Every
session wearing a latch writes to one adherence record, and the next prompt
in any session took the newest entry. Corrections now go to the session
whose reply broke the rule, and the
Stophook resolves that session's own latch before measuring instead of trusting whichever session ran last. - On Windows a state write failed whenever another process had
state.jsonopen, which another session's hook reading it is enough for, and the hook then dropped the turn's directive silently. A refused replace is now retried briefly, and every write uses its own temp file, so two hooks writing at once can no longer rename each other's half-written file into place.
0.3.0 - 2026-09-12
Added
-
A fourth layer, and the first one that reads rather than writes. A style may declare machine-checkable assertions in its frontmatter, and a
Stophook measures the finished reply against them:checks: max_sentence_words=25; forbid=let me know if; no_bulletsRules:
max_sentence_words,max_reply_lines,max_reply_chars,max_paragraphs,forbid,forbid_opening,require,no_headings,no_bullets. Modifiers can add checks, and they accumulate. -
Closed-loop correction. Breaking a check puts one short line in the next turn's nudge naming the rule that broke, instead of restating the whole style. Naming the specific failure is a stronger signal for fewer tokens. Each breach is delivered exactly once, so a host without a
Stophook cannot nag forever about one old reply. -
::statusreports adherence over the recent replies, and what broke. -
::testfails on achecksline it cannot parse. A check that is silently never enforced is the worst outcome: the style looks measured and is not. -
The five built-in styles that have mechanically checkable rules now declare them, so this works without writing a style first.
-
One-shot latches:
::terse! explain this regexapplies the rules to that message and latches nothing, so trying a style — or asking for one plain answer — no longer costs you the style you actually work in. -
A bounded record of what was latched and when.
::statusshows the last five, so "what did I switch away from" is answerable.
0.2.1 - 2026-09-12
Fixed
- The slash command is now
/styles, not/stylelatch. Claude Code lists skills and commands in one inventory, so shippingskills/stylelatchalongsidecommands/stylelatch.mdregistered two components under the same invocation name and one shadowed the other in the/menu. A test now refuses any command that shares a name with a skill.
0.2.0 - 2026-09-12
Added
- A skill at
skills/stylelatch/, auto-discovered by both Claude Code and Codex, so an agent can help you write and validate a style of your own rather than only wear one. - A
/stylelatchslash command on Claude Code, for people browsing the/menu who have not met the::grammar yet. - A portable
plugin.jsonfollowing the Agent Plugins 1.0.0 spec, alongside the two provider manifests. A test keeps all three in agreement. docs/architecture.md,docs/authoring-styles.mdanddocs/providers.md.CLAUDE_PROJECT_DIRis used as a second source for project detection, behind the working directory the hook payload carries.- Scoped latches. A latch belongs to a session, a project, or everything, and
the most specific one that is set wins.
::terse @projectkeeps a voice inside one repository;::silent-run @sessiondies with the conversation. ::off @projectclears one scope and says what takes over;::offstill stops everything.- Setting a latch that a more specific scope already outranks says so, instead of leaving the user to wonder why nothing changed.
- A scope whose key cannot be determined — no project detected, or no session id from the host — is refused rather than stored under a key nothing could look up again.
::status: what is latched, in which scope, how long ago, and what each layer costs in characters and estimated tokens.::test canary offremoves the canary and restores the latch it displaced.- Styles now come from three sources, most specific first: a project's
.stylelatch/styles/, the user's$STYLELATCH_HOME/styles/, and the built-ins. A style you write survives a plugin update, and a repository can carry the voice its contributors agreed on. - A shadowed style is reported rather than hidden.
::?and::testboth say which source won and which one lost. ::test, one diagnostic entry point reachable from the chat.::testruns every structural check and reports PASS/FAIL per component, including whether the provider is running an installed snapshot rather than your working tree.::test on/::test offdrive debug mode, which reports what each hook invocation actually received.::test canaryplants two high-entropy tokens, one per layer, so a partial delivery failure is visible rather than silent.::test verifyproves injection from the host's own transcript.
- Debug mode retires itself after 20 turns, 60 minutes, or a session change, whichever comes first, and says which limit ended it.
- Forgiving directive parsing:
::terse,:: terseand: :terseare the same thing, case is ignored, and a near miss gets a suggestion. - Continuous integration: the test suite runs on Linux, Windows and macOS
across Python 3.9 through 3.13, plus a
rufflint and format gate. - Issue templates, a pull request template, and a contributing guide.
ROADMAP.md, so the direction is visible before the code lands.
Changed
- The state file is versioned and migrates itself forward. A v1 file, which carried one unscoped latch, becomes the global latch.
state.jsonstores the recipe rather than the rendered style, and the hooks recompose from it.ACTIVE.mdbecame a mirror of the winning latch, which is what layer 1 needs and all it can use.- The canary is a real style file in the user directory rather than a document composed in memory, so it goes through exactly the machinery it is testing.
- State writes preserve keys that are not part of the latched style, so debug mode survives a style switch.
- Both hooks now build their injection from a list of parts, so a nudge and a debug block can travel together.
Removed
scripts/style.py,scripts/canary.pyandscripts/verify_injection.py. Every capability they had is now a::testsubcommand. StyleLatch is driven from chat; a terminal surface for users was a second thing to document, test, version and eventually remove.
0.1.0 - 2026-09-12
Added
- Three-layer style enforcement from one shared set of style files.
- Layer 1: one line in
AGENTS.md/CLAUDE.md, always in context. - Layer 2: a
SessionStarthook, matchingstartup|resume|clear|compact|fork. - Layer 3: a
UserPromptSubmithook carrying a ~30-word nudge.
- Layer 1: one line in
- Composition at switch time: one profile plus N modifiers merge into a single
flat
ACTIVE.mdwith the real rules written out. - The
::in-chat directive:::eli5+no-preamble,::off,::?,::debug. - State outside the plugin root, so an update never wipes the active style.
Resolution:
$STYLELATCH_HOME, then$PLUGIN_DATA, then~/.stylelatch. - Atomic state writes, and fail-open hooks that can never break a turn.
- Four profiles:
eli5,terse,deep-technical,silent-run. - Four modifiers:
full-paths,no-preamble,show-evidence,red-balls. - Diagnostics: a two-token canary, and a session-log scanner that proves injection from the host's own transcript rather than the model's testimony.