Outside a session: the commit gate, CI, and pinning
The commit gate
Section titled “The commit gate”The hooks run only inside a Claude Code session. Text reaches a repository in other ways too. A teammate commits by hand, an agent writes a file through a shell command, or a pipeline generates one. The commit gate covers those paths. It runs Vale on the lines a commit adds, and it fails the commit when any of those lines has an error-level alert. It needs git, node, and vale. It does not need the plugin, so every committer meets the same policy.
The gate has two modes:
node "<repo or plugin dir>/src/cli/check-staged.js" # pre-commit: what you are about to commitnode "<repo or plugin dir>/src/cli/check-staged.js" --base main # CI: everything a branch added since mainWhat it checks. Markdown and HTML files: .md, .mdx, .markdown, .html, and .htm. Generated HTML such as a coverage report or a built docs site enters the gate too, so exempt those paths in .vale.ini. Within the checked files, only the lines you added. A file with old problems does not fail your commit when you touch one paragraph of it. The one exception is a rule that judges the whole document, such as a metric rule for sentence length or an occurrence rule with document-wide scope. Vale reports those at line 1, so the gate keeps them whenever the file changed at all.
Pre-commit mode lints the staged content, so it sees exactly what will land. If you stage half of a file with git add -p, the gate checks that half and ignores the rest. --base mode lints the working tree and keeps the lines added since the merge-base with the ref you name, uncommitted edits included. An untracked prose file counts as added in full. That gives CI the same check over a whole branch, and --base HEAD checks exactly the lines you have not committed yet.
Exit codes:
0: clean, or only warnings and suggestions.1: at least one error-level alert on an added line. The report lists them.2:gitorvalefailed, so the line filter cannot be trusted. The gate refuses to pass a commit it could not check.
Escape hatches. To skip the gate for one commit, run JUST_SAY_SO_PRECOMMIT=0 git commit .... When vale is missing from PATH, the gate prints a notice and exits 0. Adding the gate to a repo never blocks commits on a machine without Vale.
Configuration is the .vale.ini alone: the nearest one above the file, or the shipped fallback when there is none. A term you allowed during a session lives in that config’s vocabulary, so it is allowed at commit time too.
Installing it in a repository: /just-say-so:init does it for the hook tooling the repo already has, and prints the step it cannot do itself (a dev dependency install or pre-commit install). By hand, by preference:
-
The pre-commit framework. This repo ships .pre-commit-hooks.yaml. Pin the hook at a tag, and the framework clones the script at that revision:
repos:- repo: https://github.com/ifandelse/just-say-sorev: v0.5.0hooks:- id: vale-staged -
Husky or plain
.git/hooks. Add this repo as a dev dependency from a tag, then call the bin from your hook. The lockfile is the pin, and the package has no transitive dependencies:{ "devDependencies": { "just-say-so": "github:ifandelse/just-say-so#v0.5.0" } }node_modules/.bin/just-say-so-staged
Do not point a hook at the Claude plugin cache. The path there is machine-specific and version-suffixed, and the hook silently skips every committer who lacks the plugin.
Single-prompt runs (CI)
Section titled “Single-prompt runs (CI)”Non-interactive runs (for example anthropics/claude-code-action) submit one prompt and end. These behaviors matter there:
reply.mode: "warn"delivers nothing in a single-prompt run: its report rides the next prompt, and there is none. Useblockwhen the reply must pass. Otherwise set the reply check tooffand letfilesandpublishcover the text, which is where CI output lives.rules.onContextStart: ["startup"]injects the rules once at the start, which is all a single-prompt run needs.
To run CI stricter than developers’ machines, commit a config for it and point the forced layer at that file:
env: JUST_SAY_SO_FORCE_CONFIG: ${{ github.workspace }}/.github/just-say-so.ci.json{ "rules": { "onContextStart": ["startup"] }, "files": { "mode": "warn" }, "reply": { "mode": "block" }}Fields the forced file sets win over the project file, so a mode committed to .just-say-so.json can never weaken the CI policy. Fields it leaves out fall through normally.
Pinning a version in CI
Section titled “Pinning a version in CI”anthropics/claude-code-action installs plugins from a marketplace URL or directory path. Neither form carries a branch, tag, or commit, so every run installs whatever sits on this repo’s default branch. If your repository requires pinned dependencies, that is not enough.
Until the action supports refs, pin by vendoring — copying the plugin into your repository:
-
Copy
.claude-plugin/,hooks/,rules/,skills/, andsrc/from a tagged release into your repository, for example under.github/plugins/just-say-so/. -
Point the action at that directory:
with:plugin_marketplaces: ${{ github.workspace }}/.github/plugins/just-say-so -
Record the tag you copied. To upgrade later, diff your copy against the new tag — for example
git diff v0.1.2 v0.1.3in a clone of this repo — and apply what changed.
The copy needs no install step: zero npm dependencies, Node ≥ 18.