Ruttla comes from the German rütteln ("to shake").
A normal linter reads the code as it lies. Ruttla shakes it until whatever sits loose falls out — which is exactly what the logo shows: a pair of braces being shaken, a bug tumbling out.
Code → Ruttla shakes → what sits loose falls out.
It doesn't just check whether a rule turns green — it actively tries to make every rule fail with a counter-probe. And when nothing was measured, Ruttla never claims that everything is fine.
Ruttla is a local, deterministic quality gate: executable engineering
memory for failures that normal linters miss. A real failure happens → its
cause is proven → a deterministic rule is written → a broken and a healthy
probe must both behave → from then on the failure costs no debugging or
agent tokens. Scanning uses no LLM, network, or telemetry; target code is read,
never executed. Network access happens only when explicitly running ruttla update or ruttla hub search|add|sync|submit, or when a scan is started with --allow-network: then the repository check asks the GitHub API whether the scanned repository is public (owner and repository name, nothing from the code).
Status: 0.1.0.dev0, public preview. German version: README.de.md.
Coding agents (Claude Code, Cursor, OpenAI Codex, GitHub Copilot, Aider, Windsurf) waste thousands of tokens looping on subtle bugs and linters' blind spots. Ruttla provides:
- Token-dense output (
--format agent): One line per finding withfile,line,severity, and an exact, non-hallucinated fix instruction (FIX: ...). - Zero token burn: Scans are offline and deterministic; no network calls or LLM queries unless you explicitly run
ruttla update, aruttla hubcommand that fetches, or a scan with--allow-network. - Copy-paste agent setup: docs/AGENT_SETUP.md is one prompt for any coding agent — install, gate at the end of every task, turn a reported bug into a project rule, and what never to do without asking.
- Strict exit contracts: Differentiates between clean (
0), blocking findings (1), unmeasured scopes (2), and runner errors (3). Never confuses "nothing checked" with success.
curl -fsSL https://raw.githubusercontent.com/hehljo/Ruttla/main/install.sh | sh # Linux, macOS
ruttla path/to/project # human-readable report
ruttla . --format agent # one line per finding, for scripts and agents
ruttla . --format json # full report (schemas/report.schema.json)
ruttla . --sarif ruttla.sarif # SARIF 2.1.0 for GitHub code scanning
ruttla . --changed-only origin/main # only changed + untracked files
ruttla --list [--format json|markdown] # rule catalog
ruttla --explain secrets.hardcoded_credential
ruttla --self-test # shake the gates themselvesFrom a checkout without installing: python3 master_gate.py … (identical).
Windows (PowerShell): irm https://raw.githubusercontent.com/hehljo/Ruttla/main/install.ps1 | iex.
The installer creates its own Python environment (Python ≥ 3.11 required) and
installs the package and the matching Rust engine from the latest GitHub
release, checked against SHA256SUMS.
ruttla . needs no configuration. It detects the platforms from marker files
(src/ruttla/platforms.toml, e.g. *.xcodeproj, package.json,
project.godot, *.csproj) per subproject: in a monorepo an iOS app in
ios/ and a web app in web/ each get their own rule pack, and no rule sees
the files of another platform's subproject. Platforms without a rule pack are
listed in the report (platforms_without_pack) instead of silently passing.
[project] platform in .ruttla.toml forces a pack on in addition.
New rules are data: one TOML file per rule under src/ruttla/rules/
(docs/RULE_FORMAT.md), executed by ruttla-engine
(Rust, linear-time regex crate, files in parallel). Rules that need parsing
or state across files remain Python checks.
The installer and ruttla update put a prebuilt engine next to the ruttla
script (Linux x86_64/arm64, macOS arm64/x86_64, Windows x86_64). Without an
engine the declarative rules are reported as unmeasured, never as green. On
other platforms build it yourself (Rust ≥ 1.85):
cargo build --release --locked --manifest-path engine/Cargo.toml
export RUTTLA_ENGINE_BIN="$PWD/engine/target/release/ruttla-engine" # for a pip install elsewhereruttla rule test PATH checks a rule or rule package before it is merged or
shared: isolated load, fixtures, an untested-exclusion probe, ID clashes and
a runtime budget (docs/RULE_FORMAT.md).
Rule packages from the community hub (hehljo/ruttla-hub)
are signed data, pinned in ruttla-hub.lock and loaded offline:
ruttla hub search, ruttla hub add NAME, ruttla hub sync. Your own rules go
to .ruttla/packages/NAME/ in the project — they run in every scan, no hub
command or update overwrites them, and ruttla hub submit NAME --yes opens the
PR that makes them available to everyone
(docs/RULE_FORMAT.md).
A checkout finds its own build without the variable. The engine is never
looked up on PATH or in the scanned directory. --engine required turns a
missing engine into a runner error (exit 3) — use it in CI.
Run ruttla update to install the package and the matching engine from the latest GitHub release into the active Python environment — both from the same commit, the engine checked against SHA256SUMS. Without a tagged release it takes nightly, built from the last green CI run on main (--channel stable|nightly to choose). This explicit command uses pip and requires network access; ordinary scans remain offline. It replaces only the installed package and engine — project files (.ruttla.toml, ruttla-hub.lock, .ruttla/) stay untouched.
Before any download it stops on an editable clone (pip install -e . — update the checkout with git pull instead) and on a system Python marked externally managed (PEP 668 — install Ruttla in a venv or with pipx). The engine is verified before pip runs, so a rejected engine never leaves a half update.
| Code | Meaning |
|---|---|
| 0 | measured, no blocking finding (advisory findings may exist) |
| 1 | at least one blocking finding |
| 2 | nothing was measured — not a success |
| 3 | runner, input, config or plugin error |
Capture the status before filtering: ruttla . | head reports head's status.
out=$(ruttla . --format agent); status=$?Every finding carries blocking: true|false. Without a profile only rules
marked safe by default block; with a profile every error blocks; with
--strict every finding blocks. Details: docs/CONTRACTS.md.
config_version = 1
[gate]
strict = false
exclude = ["legacy/**", "extracted/**"] # glob on the relative path
max_file_bytes = 2000000 # larger files are reported as skipped
[project]
platform = "apple" # force a rule pack on (in addition to detection)
[brand]
names = ["Acme"]
sources = ["src/brand.ts"]
[severity]
"secrets.*" = "error"
"godot.untyped_declaration" = "off"The legacy name .qualitygate.toml is still read; both files at once is an error.
A finding that stays on purpose goes into .ruttla/baseline.toml: one entry
per fingerprint with a count, a reason and an expiry date (at most a year).
ruttla baseline create --reason … --expires YYYY-MM-DD records today's
findings once when adopting Ruttla; after that each entry is added on its own
with ruttla baseline add --check ID --file PATH …. Expired entries stop
suppressing, entries without a finding are reported. ruttla baseline show
lists them.
--trusted-ref REF reads the profile and the accepted findings from REF
(e.g. origin/main) instead of the working tree, so a branch cannot switch a
rule off or accept its own finding. ruttla guard --base REF exits 1 when a
change touches the gate policy (profile, .ruttla/, workflows, CODEOWNERS,
.gitignore), adds suppression comments or removes tests — the acceptance
check for automated fixes. ruttla triage REPORT.json … --out DIR turns JSON
reports into fix tasks per repository and fix class (mechanical, catalog,
judgement, security; table in src/ruttla/fix_classes.toml, unlisted rules
default to judgement). A shared CI workflow for many repositories is in
docs/integrations/github-actions.md.
- Four honest states: pass, fail, unmeasured (with a reason), error. "Zero files checked" is never green.
- Self-tested rules: every rule must FAIL on its broken probe and PASS on
its healthy probe (
ruttla --self-test); current counts per pack in the generated docs/RULES.md. - Evidence-backed catalog: rules come from real failures, see docs/RULES.md, docs/GUIDELINES.md and LESSONS_LEARNED.md.
- Coverage is visible: skipped oversize files and platforms without a rule pack are reported, not silently dropped.
- Agent-native output: stable check IDs, compact agent format, JSON, SARIF.
Packs: universal (brand, i18n, secrets, gates, docs, media, protocol), apple (incl. App Store release), web, python services, raspberry, godot, unreal, dotnet (WPF/Avalonia).
Ruttla does not replace compilers, CodeQL / GitHub Code Quality, Ruff, ESLint or SwiftLint. Use those for the mainstream issues they cover well; use Ruttla for cross-toolchain failure classes, release/store workflows and project knowledge they don't encode. Upload each tool's SARIF under its own category (docs/integrations/github-actions.md).
Known limitation: rule messages are currently German.
Offline, no telemetry. The scan target is untrusted data: nothing from it is
imported or executed, and symlinks may not leave the scan root. Executable
checks load only from the installed package; from the target Ruttla reads
declarative rules only — hub packages whose hash matches ruttla-hub.lock and
the project's own packages under .ruttla/packages/ — which can only add
findings (ADR-0005).
ruttla stats enable starts recording scans on this machine only; nothing is
recorded before that and nothing is ever sent anywhere. The SQLite file lives
in the user data directory ($XDG_DATA_HOME/ruttla, ~/Library/Application Support/ruttla, %APPDATA%\ruttla; override with RUTTLA_STATS_DIR), never
in the scanned repo. Stored: counts per scan, rule and severity, CI vs. local
(CI variable), and per finding an HMAC fingerprint over rule, relative path
and normalised evidence with a local salt. Never stored: source text, evidence,
messages, secrets, plain paths or repo names — a project is a local random ID.
A finding counts as fixed only when the scan re-examined the spot (rule ran
and measured, file still in scope); otherwise it is indeterminate. Report,
reporters and exit code are identical with and without statistics; a
statistics error never fails a scan. ruttla stats [--period 7|30|90|all]
shows the dashboard (headed by the all-time balance: distinct problems found,
fixed, regressions, runs with a blocking finding — counted per fingerprint, not
per scan), stats export writes JSON and Markdown, stats disable
stops recording and keeps the data, stats delete --yes removes it. Scans
below the system temp directory (test runs, throwaway clones) are not recorded.
ruttla . --lifecycle appends this scan's lifecycle to the text report —
new · persisting · reappeared (regression) · fixed · indeterminate, with the
rules behind regressions, new and fixed findings. Without recording it says
not measured, never "0 new"; JSON, SARIF and agent output stay unchanged.
For a public record, ruttla badge update writes a card from a full scan:
current and history badge (shields.io), history.jsonl and a summary page —
counts and rule IDs only, each line tied to commit, Ruttla version and CI run,
history starting at the first recorded scan. Opt-in per repository via a
workflow that publishes to its own branch:
docs/integrations/stats-badge.md.
See CONTRIBUTING.md — a new rule needs a real failure, a broken and a healthy probe, a false-positive analysis and a fix hint that only names APIs that exist. Write it as a TOML rule (docs/RULE_FORMAT.md) unless it cannot be expressed as a pattern. Security: SECURITY.md. License: Apache-2.0.