Agentic-native linting tool to enforce deterministic quality gates
> /plugin marketplace add ironlint/ironlint> /plugin install ironlint@ironlint-marketplace
Repo: ironlint/ironlint
What's inside
IronLint is a local policy gate for agentic coding harnesses. GitHub Actions runs your checks in the cloud after you push; IronLint runs the same kind of checks locally, on every edit your agent makes, before the code lands on disk, and it can refuse. A check is a file glob plus a shell command. IronLint hands that command your proposed content on stdin and reads one thing back: the exit code. Any nonzero exit (1–125) blocks the write. No engines, no severities, no output parsing.
One YAML file at .ironlint.yml in your repo root. A map of checks; each names what to match, where it applies, and the command that decides.
# .ironlint.yml
checks:
no-console:
files: "**/*.ts"
run: "! grep -n 'console.log'" # proposed content arrives on stdin
lint-and-format:
files: "src/**/*.py"
run: "ruff check --quiet --stdin-filename \"$IRONLINT_FILE\" -"
New here? Start with Getting started.
Prebuilt binaries for macOS (Apple Silicon and Intel), Linux (x86-64), and Windows (x86-64). The installer downloads the right binary, drops it in ~/.cargo/bin, and puts it on your PATH — no Rust toolchain required:
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/ironlint/ironlint/releases/latest/download/ironlint-cli-installer.sh | sh
Windows (PowerShell):
powershell -ExecutionPolicy Bypass -c "irm https://github.com/ironlint/ironlint/releases/latest/download/ironlint-cli-installer.ps1 | iex"
Or build from source (needs a Rust toolchain):
cargo install --git https://github.com/ironlint/ironlint ironlint-cli
Then run ironlint --version. Update any time with ironlint update (installer-based installs; source builds update with cargo install … --force).
ironlint init takes you from clone to gated in one command. It scaffolds a starter .ironlint.yml, trusts that new config, detects your coding agents, and wires IronLint's edit hook into each — so policy runs on every edit without you calling ironlint check by hand:
ironlint init
It detects Claude Code, Codex, pi, and OpenCode, asks before touching anything, and installs the hook. Target one explicitly, wire them all, or patch your user account instead of the project:
ironlint init --harness opencode # just one agent
ironlint init --harness all # every supported agent
ironlint init --global # wire user-level agent settings (the config remains project-local)
It also installs an ironlint-config authoring skill so the agent knows how to write checks; run ironlint schema to read the format yourself. ironlint doctor verifies the wiring (one row per agent); ironlint init --uninstall --harness <name> removes it.
Sharing checks across repos:
extends: ["../shared/ironlint-base.yml"]
checks:
# project-specific overrides + additions
Local checks override inherited checks of the same id. Full format: ironlint schema or Configuration.
steps:). IronLint reads one thing back — the exit code. 0 passes, 1–125 blocks, and your command's stdout/stderr becomes the block message. No engines, no severities, no output parsing.ruff, eslint, tsc, clippy, biome — the same checks CI runs, wired in as-is. No per-tool integration to write.on: [write] fires per edit; on: [pre-commit] fires once over the selected matching file set. A single check can do both — IronLint keys by check, not by event.ironlint trust blesses it; any change to the config or its check scripts revokes trust until you re-bless. check fails closed on untrusted input..ironlint/log.jsonl; the directory ignores itself. The optional standalone Claude Code plugin provides the /ironlint-review skill for reviewing noisy and dead checks; it is not a CLI command or part of the standard init integration.An LLM catches its own slop poorly, but fixes it well once something reliable points at the problem. IronLint is the deterministic half of that loop: a check runs on every edit and hands back a signal the model can't talk its way around — a nonzero exit. The agent writes, the check judges, the agent fixes. Push every fixed-rule quality gate onto deterministic checks, and leave prompts and skills for the judgment calls that genuinely need them.
That ethos has a name and a longer argument — Determinism-in-the-loop.
A check fires on write (default), pre-commit, or both.
| Event | Trigger | stdin | $IRONLINT_FILE | $IRONLINT_FILES |
|---|---|---|---|---|
write | Every agent edit | proposed content | the edited file | same, single entry |
pre-commit | Once for the selected matching file set | empty | not set | selected matching files, newline-joined |
Use on: [write, pre-commit] to fire at both — no duplication. This is the inversion that separates IronLint from lefthook, whose earliest reach is pre-commit, after the agent already wrote the file.
Every check receives, on its environment:
$IRONLINT_FILE — absolute path of the file under check (set for write; not set for pre-commit).$IRONLINT_FILES — newline-joined list of all selected files (single entry for write; the matching file set for pre-commit).$IRONLINT_ROOT — project root (the check's cwd).$IRONLINT_EVENT — write or pre-commit.$IRONLINT_BIN — absolute path to the ironlint binary, so a check can invoke it without PATH resolution.$IRONLINT_TMPFILE — a materialized temp file holding the proposed content, set only when your run/steps reference it.$IRONLINT_PROPOSED_MANIFEST — optional tab-separated manifest of sibling proposed files in the same atomic patch; absent unless an adapter supplies it.write) or empty (pre-commit).Read proposed content from stdin, not from $IRONLINT_FILE. On harnesses that gate before the write lands (codex, pi), the file on disk still holds the old content.
┌──────────────────────────────────────────────────────────────┐
│ Agent Edit / Write (or git pre-commit) │
│ ↓ │
│ harness adapter hook │
│ ↓ │
│ ironlint check │
│ ↓ │
│ match touched file → checks whose `files:` glob hits │
│ ↓ │
│ for each check: sh -c <run> │
│ env: $IRONLINT_FILE $IRONLINT_ROOT $IRONLINT_EVENT … │
│ stdin: proposed post-edit content │
│ ↓ │
│ read only the exit code │
│ 0 → pass 1–125 → BLOCK the write │
│ 126 / 127 / timeout / signal → InternalError (never a │
│ silent pass) │
└──────────────────────────────────────────────────────────────┘
Each adapter collapses one harness's edit hook into the ABI above and runs ironlint check. The runner matches the touched file to checks, dispatches each (once per file on write, once per check on pre-commit), and folds the exit codes into a single verdict.
| Code | Meaning |
|---|---|
| 0 | Pass — every matched check passed |
| 1 | Config or load error — parse failure, missing file |
| 2 | Block — at least one check exited nonzero (1–125) |
| 3 | InternalError — at least one check crashed (not found, timeout, killed by signal) |
| 4 | Untrusted config/checks — run ironlint trust |
Adapters fail-open on exit 3 by default. Opt-in fail-closed: IRONLINT_FAIL_CLOSED_ON_INTERNAL=1. Exit 4 is different: adapters must surface it loudly, and pre-write adapters (every shipped adapter) treat it as fail-closed and block the tool call outright — an untrusted config is never silently allowed through.
The codex adapter reads these same exit codes but doesn't block through its own exit code — it translates them into a permissionDecision:"deny" JSON object on stdout, because that's how Codex's PreToolUse hook contract blocks a tool call. See adapters/codex/README.md.
Never run a check or write telemetry. Exit 0 on success, 1 on a config error — never 2.
ironlint explain <file> # which checks are in scope for a file, and their run commands
ironlint show-resolved-config # the post-extends: merged check set, annotated by source
ironlint schema # the check-authoring guide
All honor --config <path> (default .ironlint.yml).
Add # ironlint-disable: <check-id> anywhere in a file to suppress that check for the whole file.
adapters/claude-code/. PreToolUse hook for edits, plus three skills. See docs/adapters/claude-code.md.adapters/opencode/. tool.execute.before gates proposed edits. See docs/adapters/opencode.md.adapters/codex/. PreToolUse hook for apply_patch, blocking via a permissionDecision JSON on stdout rather than an exit code; requires a one-time review+trust step inside Codex. See adapters/codex/README.md.FAQ
ironlint is a Claude Code plugin with 3 hand-picked skills for development work, indexed on Flowy. Install it with the command on its page. It includes ironlint-init, ironlint-review, ironlint. Its skills do not fire on their own yet. Request auto-invocation to have Flowy route them as you prompt. Free and open source.
Is this plugin yours?
Claim it with GitHubSubmit a pluginPromote it