Auto-lint-on-edit hook: silent autofix via oriz-lint-file dispatcher (2026-07-04)
Auto-lint-on-edit — 2026-07-04
Locked
- Trigger: PostToolUse hook on
Edit|Write|MultiEdit. Fires within milliseconds of every file write. - Speed: Native per-lang formatters via
npx/uvx(no Docker cold-start). Not MegaLinter Docker — that stays at CI layer perfleet-ci-megalinter-fork-external-2026-07-03. - Autofix: Silent — file is reformatted before the next tool call sees it.
- Scope: Repo config wins when present (
biome.json,ruff.toml,.prettierrc,Cargo.toml); else fleet defaults from~/.oriz/lint/. - Forks: Skip all
repos/frk/**perno-fork-divergence. - Timeout: No cap — grill-locked.
- Agents: CC hook wired natively; shared
oriz-lint-file.mjsCLI callable by all fleet agents.
Dispatcher
scripts/oriz-lint-file.mjs — Node ESM, stdlib only. ~250 lines. Reads path from argv, --stdin JSON (CC hook shape), or $CLAUDE_FILE_PATH env var.
Formatter map
| Extension | Formatter | Command | Repo config detected via |
|---|---|---|---|
.js .jsx .ts .tsx .mjs .cjs .json .jsonc |
biome | npx -y @biomejs/biome check --write |
biome.json, biome.jsonc |
.py |
ruff | uvx ruff check --fix --exit-zero + uvx ruff format |
ruff.toml, .ruff.toml, pyproject.toml |
.md .mdx .yml .yaml .css .scss .html .vue .svelte .astro |
prettier | npx -y prettier --write |
.prettierrc*, prettier.config.{js,mjs} |
.go |
gofmt + goimports | gofmt -w + goimports -w |
none needed |
.rs |
cargo fmt | cargo fmt --manifest-path <nearest> |
Cargo.toml (via walk-up) |
.sh .bash |
shfmt | shfmt -w -i 2 |
none |
.github/workflows/*.yml |
actionlint | actionlint <file> |
none (report only) |
Excludes
Any of the following → skipped:
- Path segment matches:
node_modules,.git,dist,build,.next,__pycache__,.venv,venv,target,coverage,.staging,.staging-*,.obsidian,.agents,.idea,.vscode-test,.pytest_cache,.mypy_cache - Sequence
repos/frk/→ excluded-fork (protectsno-fork-divergence) - Filename patterns:
*.min.js,*.min.css,*.generated.*,*_pb2.py(protobuf),*.g.dart
Fleet-default configs at ~/.oriz/lint/
Written on first install; used only when a repo has no local config for that language.
biome.json— 2-space, 100-char line width, single quotes, no semis, trailing commasruff.toml— 100-char line, py311 target, black-compat, E/F/I/W/UP/B/SIM/PT rules.prettierrc— 100-char, 2-space, no semis, single quotes, trailing commas; MD override to 120-char + preserve prose wrap; YAML override to double-quotes
Hook wiring
CC (this canonical + global):
"PostToolUse": [
{ "hooks": [{ "type": "command", "command": "cavemem hook run post-tool-use --ide claude-code" }] },
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [{ "type": "command", "command": "node C:/D/oriz/scripts/oriz-lint-file.mjs --stdin" }]
}
]
CC pipes the tool input as JSON to stdin. The dispatcher parses:
tool_input.file_path(Edit/Write)tool_input.edits[].file_path(MultiEdit)
Other agents: wire when their hook APIs mature. For now, invoke manually:
node C:/D/oriz/scripts/oriz-lint-file.mjs /path/to/edited-file.ts
Grill locks
- Trigger — PostToolUse hook (not Stop, not pre-commit). Immediate feedback.
- Formatter — native per-lang. MegaLinter Docker rejected as too slow for per-write feedback.
- Autofix — silent autofix. No agent judgment call on style.
- Scope — fleet defaults with repo-config override. Repo config wins when present.
- Forks —
repos/frk/**skipped entirely. Formatting a fork = local diff vs upstream =no-fork-divergenceviolation. - Fleet-defaults override — clarified as "use repo config if present, else fleet default".
- Agents — CC hook + shared CLI. OpenCode/Kilo/Cline call the CLI manually until their hook APIs support the pattern.
- Verify — 5 file types tested (.ts, .py, .md, .yml, .sh). Biome + ruff live-verified as autofix-working. Fork exclusion live-verified.
- Timeout — no cap. Formatters allowed to complete; corp-Windows npx cold-start can be 5-10s on first run, cached after.
Live-verify results (2026-07-04)
| File type | Path tested | Formatter | Result |
|---|---|---|---|
.ts |
/tmp/lint-test/test.ts |
biome | ✅ Reformatted: quotes " → ', added semis where needed removed elsewhere, split one-liners |
.py |
/tmp/lint-test/test.py |
ruff | ✅ Deleted unused imports (os, sys), fixed spacing, 4-space indent |
.md .yml .sh |
in .staging/ |
(skipped) | ✅ Correctly excluded per .staging filter |
| Fork file | repos/frk/screenpipe/README.md |
(skipped) | ✅ Correctly excluded per repos/frk/ filter |
Non-goals (deliberately deferred)
- MegaLinter Docker local runs — CI layer only; too slow for per-write.
- Linter reporting to agent — grill locked "silent autofix" not "report + let agent decide"; skipping the report path.
- markdownlint — grill-flagged as too opinionated; using prettier's MD handling instead.
- stylelint — CSS handled by prettier; deeper stylelint integration deferred.
- shellcheck — shfmt formats but doesn't lint; shellcheck can be added later if bash scripts grow.
- Golangci-lint / clippy — beyond format; adds lint layer that current CI already covers.
Escape hatches
- Disable per-turn: unset
ORIZ_LINT_VERBOSEandmv scripts/oriz-lint-file.mjs scripts/oriz-lint-file.mjs.disabled— CC will silently no-op the hook. - Disable per-file: add a
// prettier-ignore,# noqa, or// biome-ignorecomment; each formatter respects its own ignore syntax. - Skip via env:
ORIZ_LINT_SKIP=1env var — not wired in v1; add if needed.
Related decisions
fleet-ci-megalinter-fork-external-2026-07-03— CI-layer MegaLinter (complementary, not duplicated)agent-mcps-canonical-2026-07-04— sibling automationno-fork-divergence— why forks are excluded