Skip to content

Git hooks and CI

The same rule reports the same line the same way in an agent hook, a git hook and CI. What counts as new is a match on a line you changed; a violation that was already there is backlog, shown and never failing.

  • Pre-commit: rulecast run with no file flags checks the staged files, reads their content from the index (what is being committed, not the working tree), and judges it against HEAD. llm rules are skipped unless you pass --llm, and one line says so: a commit should not cost money.
  • Pre-push and CI: rulecast run --from-ref A --to-ref B checks the files changed between the merge base with A and B, reading them at B. Without --to-ref it reads the working tree.
  • rulecast run --files F... and --all-files judge files whole: against a committed .rulecast/baseline.json when there is one, else with no baseline. Use the hook below rather than --files from a hook manager.
  • A nightly sweep or a scheduled agent loop over the whole repository has no ref to diff against: commit a baseline once, and rulecast run --all-files exits 0 unless something got worse. Recipes for /goal, /loop and scheduled sweeps: agents/reference/loops.md.

rulecast installs no git hooks itself: lefthook, husky and pre-commit already own .git/hooks. Add @syv-ai/rulecast to the project’s devDependencies (the agent hooks need it there anyway), and wire it into the one you use.

pre-commit (.pre-commit-config.yaml):

minimum_pre_commit_version: "3.2.0"
repos:
- repo: https://github.com/syv-ai/rulecast
rev: v0.4.0
hooks:
- id: rulecast
- id: rulecast-push

lefthook (lefthook.yml):

pre-commit:
jobs:
- name: rulecast
run: npx --no-install rulecast run
pre-push:
jobs:
- name: rulecast
run: npx --no-install rulecast run --from-ref "$(git merge-base origin/HEAD HEAD)" --to-ref HEAD

GitHub Actions:

on: pull_request
jobs:
rulecast:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # rulecast needs the merge base with the target branch
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npx rulecast run --from-ref origin/${{ github.base_ref }} --format sarif > rulecast.sarif
- uses: github/codeql-action/upload-sarif@v3
if: always() # rulecast exits 1 on findings and 3 when a rule could not run, which is when the upload matters
with:
sarif_file: rulecast.sarif

A shallow clone has no merge base; rulecast says so and names fetch-depth: 0. --from-ref runs llm rules, which judge changed files only.

command scripts, and oxlint, are handed a scratch copy of each file in staged and --to-ref runs, because the content to judge is not the file on disk. ruff and eslint get it on stdin under the real path, so path-keyed configuration still applies. Whole-project tools (scope: project, hook rules) need the real project: they run on the working tree when the rule’s files there match what is being committed, and are skipped with a warning naming the files when they don’t. rulecast never stashes.

A CI job, a cloud agent session or a new container starts with nothing installed. rulecast does not fail quietly there: in unattended mode every stop says which rules did not run and why (“2 rules disabled: …: not in the cache”), and the agent is held once per prompt to put that in its final report. rulecast run says incomplete and lists them under disabled in JSON. To have nothing to report:

  1. Install dependencies (npm ci or pnpm install) before the agent starts. Without them the agent hooks do nothing, and in unattended mode every stop says so.
  2. Run npx rulecast install. It fetches the pinned rule repos; hooks never fetch.
  3. Run npx rulecast doctor --ci before the agent step. It reads the cache as the hooks do and fetches nothing. It exits 2 if a rule repo is missing from the cache, a rule’s tool is not installed, or the hooks cannot run (a committed hook command runs only node_modules/.bin/rulecast, so a global or npx rulecast does not count). Add --from-ref origin/main when the job also runs rulecast run --from-ref.
  4. Check out with fetch-depth: 0 if you use --from-ref.
  5. Under CI or GITHUB_ACTIONS unattended mode is on by itself. Elsewhere set RULECAST_MODE=unattended. The hook command’s not-installed guard reads only the environment, never the config’s mode:.
  6. End the loop with rulecast run --from-ref …. It is the gate that counts, and a non-zero exit with incomplete means some rules did not run.
  7. Optionally cache ~/.cache/rulecast/repos between jobs (for example actions/cache keyed on .rulecast-config.yaml). It saves a clone per job.

A new git worktree on the same machine needs none of this beyond step 1. Rule repos are cached per user, not per checkout, and nothing on the edit path reads a per-project cache (Performance). rulecast warm only matters for plugin detectors that warm, and SessionStart starts it for you.