ironlint

skill
Security Audit
Fail
Health Warn
  • License — License: Apache-2.0
  • Description — Repository has a description
  • Active repo — Last push 0 days ago
  • Low visibility — Only 7 GitHub stars
Code Fail
  • rm -rf — Recursive force deletion command in .codex/hooks.json
  • rm -rf — Recursive force deletion command in adapters/codex/hooks/hook.sh
  • process.env — Environment variable access in adapters/opencode/src/index.ts
Permissions Pass
  • Permissions — No dangerous permissions requested

No AI report is available for this listing yet.

SUMMARY

Agentic-native linting tool to enforce deterministic quality gates

README.md

IronLint

IronLint demo

IronLint: Checks Your Coding Agent Can't Skip

Every edit your agent makes runs your checks first. Any nonzero exit blocks the write. Fully local, built with Rust.

CI Latest release Apache 2.0 Built with Rust Agent adapters

Install · Connect your agent · Configuration · What it does · Why This Works · Architecture · Docs


IronLint is local linting tool 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"
    on: [write, pre-commit]           # write: per file; pre-commit: once, with $IRONLINT_FILES
    steps:
      - name: ruff
        run: "ruff check --quiet --stdin-filename \"$IRONLINT_FILE\" -"
      - name: no-todo
        run: "! grep -n 'TODO' $IRONLINT_FILES"

New here? Start with Getting started.

Install

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/christopherarter/ironlint/releases/latest/download/ironlint-cli-installer.sh | sh

Windows (PowerShell):

powershell -ExecutionPolicy Bypass -c "irm https://github.com/christopherarter/ironlint/releases/latest/download/ironlint-cli-installer.ps1 | iex"

Or build from source (needs a Rust toolchain):

cargo install --git https://github.com/christopherarter/ironlint ironlint-cli

Then run ironlint --version. Update any time with ironlint update (installer-based installs; source builds update with cargo install … --force).

Connect your agent

ironlint init takes you from clone to gated in one command. It scaffolds a starter .ironlint.yml, trusts it, 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             # user-level settings, not the project

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.

What IronLint does

  • Runs on the write, not after. Every agent edit fires your checks with the proposed content on stdin, before it lands on disk. A nonzero exit refuses the write. GitHub Actions can't reach that early.
  • The check owns the verdict. A check is a file glob plus a shell command (or 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.
  • Your existing linters, unchanged. ruff, eslint, tsc, clippy, biome — the same checks CI runs, wired in as-is. No per-tool integration to write.
  • Two lifecycles, one config. on: [write] fires per edit; on: [pre-commit] fires once over the whole staged set. A single check can do both — IronLint keys by check, not by event.
  • Trust before it runs. IronLint refuses an untrusted config. ironlint trust blesses it; any change to the config or its gate scripts revokes trust until you re-bless. check fails closed on untrusted input.
  • Telemetry, no setup. Every run appends a JSONL record to .ironlint/log.jsonl; the directory ignores itself. ironlint-review flags noisy and dead checks.

Why This Works

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.

Lifecycles

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 before a commit empty not set all staged 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.

The check ABI

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 files (single entry for write; all staged files for pre-commit).
  • $IRONLINT_ROOT — project root (the check's cwd).
  • $IRONLINT_EVENTwrite or pre-commit.
  • $IRONLINT_TMPFILE — a materialized temp file holding the proposed content, set only when your run/steps reference it.
  • stdin — proposed post-edit content (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.

Architecture

┌──────────────────────────────────────────────────────────────┐
│           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.

Reference

Exit codes (ironlint check)
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/gates — 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.

Inspect (read-only commands)

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).

Disable a check

Add # ironlint-disable: <check-id> anywhere in a file to suppress that check for the whole file.

Adapters
  • Claude Codeadapters/claude-code/. PostToolUse hook, plus three skills. See docs/adapters/claude-code.md.
  • OpenCodeadapters/opencode/. tool.execute.before gates proposed edits. See docs/adapters/opencode.md.
  • Codexadapters/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.
  • piadapters/pi/. tool_call hook gates proposed edits before they're written. See adapters/pi/README.md.
  • Aider, pre-commit, MCP — planned.
vs lefthook

At the pre-commit boundary, IronLint is a near line-for-line swap for lefthook's gate role:

# lefthook.yml                          # .ironlint.yml
pre-commit:                             checks:
  commands:                               prettier:
    prettier:                               files: '**/*.{ts,css,md}'
      glob: "*.{ts,css,md}"                 on: [pre-commit]
      run: prettier --check {staged_files}  run: prettier --check $IRONLINT_FILES

Absorbed: the gate role. Declined: parallel, stage_fixed, and the fixer/restager half. Added: the write lifecycle — lefthook's earliest reach is pre-commit, after the agent already wrote the file; IronLint fires on the write itself.

Build from source
cargo build --release
./target/release/ironlint --version

Documentation

Full docs live in docs/ — start with Getting started or the Architecture page.

Contributing

Issues and PRs welcome. Good places to start: a new adapter under adapters/, a check pack under tests/fixtures/, or docs under docs/. See AGENTS.md for build, test, and coverage conventions.

License

Apache 2.0. See LICENSE.

Reviews (0)

No results found