precommiteu CLI
precommiteu scan src/ --fail-on-findings --sarif findings.sarif
precommiteu is a local-first EU regulatory compliance scanner for source code. All inference runs on your machine; no source code leaves it. The scan subcommand is the entire CLI surface (plus a top-level --version).
precommiteu --version
precommiteu scan --help
Flag reference: precommiteu scan
| Flag | Expects | Default | Purpose |
|---|---|---|---|
paths (positional) | one or more files/directories | none | What to scan. Omit when using --ci. |
--ci | flag | off | Scan only files changed vs the merge-target branch named by $GIT_MERGE_TARGET_BRANCH (default main). Mutually exclusive with positional paths. |
--regulations | comma-separated pack names | gdpr | Regulation packs to run. Each name must resolve to an installed regulation pack. |
--models-dir | directory path | unset | Directory holding the model files (base.gguf, <regulation>/detector-adapter.gguf). Overrides $PRECOMMITEU_MODELS_DIR. |
--orchestrator-model | GGUF file path | base.gguf under the models dir | Base model used by the orchestrator deep-agent and validator. |
--detector-adapter | GGUF file path | <regulation>/detector-adapter.gguf under the models dir | LoRA adapter applied to the detector server. |
--detector-grammar | GBNF file path | built-in grammar | Pins the output grammar for the detector call (expert). |
--agent-mode | auto | direct | orchestrator | auto | Per-file analysis route. auto = direct unless the file references sibling files. |
--gpu-layers | integer | 99 | Model layers offloaded to the GPU (99 = all, 0 = CPU only). |
--threads | integer | auto | CPU threads for inference. |
--n-ctx | integer (tokens) | 32768 | Model context window. Lower values reduce memory use. |
--json-out | file path | unset | Write the full machine-readable result (findings, advisories, statuses) as JSON. |
--report | file path | unset | Write a JSONL event ledger of the scan (progress events and findings, for audit/debugging). |
--sarif | file path | unset | Write a SARIF 2.1.0 report for code-scanning UIs. |
--out | file path | unset | Write a markdown summary suitable for a PR comment. |
--log-file | file path | precommiteu_scan.log | Append a timestamped scan log (progress events, warnings, errors). |
--progress | text | jsonl | none | text | Progress output on stderr. |
--max-orchestrator-iterations | integer | 12 | Maximum agent steps per file. |
--max-wall-seconds-per-file | integer (seconds) | 90 | Wall-clock budget per file. |
--fail-on-findings | flag | off | Exit 1 when confirmed findings remain after ignore filtering. Advisories never affect the exit code. |
--fail-on-error | flag | off (on under --ci) | Exit 3 when any file could not be scanned, so an incomplete scan never reports a clean pass. |
--show-advisories | flag | off | Print unconfirmed detector candidates (informational, never blocking). |
--max-file-bytes | integer (bytes) | 1000000 | Skip files larger than this (0 = no limit). |
--force | flag | off | Overwrite existing output files instead of refusing to run. See Never overwriting your files. |
--dry-run | flag | off | Print the files that would be scanned and exit without loading any model. |
Basic scans
Scan one path:
precommiteu scan src/
Scan multiple paths (files and directories mix freely):
precommiteu scan src/ api/handlers.py infra/main.tf
Preview the file selection without loading any model:
precommiteu scan src/ --dry-run
--dry-run prints exactly the files a real scan would process, after all filtering: only recognized source files are selected; test files, documentation, generated files, binaries, dependency directories, files over --max-file-bytes, and anything matched by .eu-ignore are skipped.
Scan with a different regulation pack, or several at once:
precommiteu scan src/ --regulations gdpr
precommiteu scan src/ --regulations gdpr,dora
CI mode, scan only the files changed vs a merge-target branch:
GIT_MERGE_TARGET_BRANCH=${GITHUB_BASE_REF:-main} precommiteu scan --ci --fail-on-findings
--ci is CI-platform-agnostic: it reads only $GIT_MERGE_TARGET_BRANCH (default main), resolves it locally or as origin/<branch>, and scans files added or modified in git diff <target>...HEAD. Wire your CI platform’s variable through GIT_MERGE_TARGET_BRANCH yourself. Passing positional paths together with --ci is an error.
Output reports
All four report flags can be combined in one run; each writes a different artifact:
precommiteu scan src/ \
--json-out result.json \
--sarif findings.sarif \
--out pr-comment.md \
--report scan-events.jsonl \
--log-file scan.log
| Flag | Format | Contents | Write behavior |
|---|---|---|---|
--json-out | JSON | Complete result: findings (including suppressed ones, with eu_ignore_reason / eu_ignore_source set), statuses (per-regulation counters: chunks_scanned, detector_candidates, validator_rejected), advisories (with retrieval annotation fields) | Snapshot, rewritten atomically after every file and finding, so partial results survive an interrupted scan |
--sarif | SARIF 2.1.0 | Findings only, and only those whose cited article resolves in the regulation pack’s article registry. One SARIF rule per article id with a helpUri link to the article text. Suppressed findings carry properties.eu_ignored and properties.eu_ignore_reason. Advisories are not included | Snapshot, rewritten atomically during the scan |
--out | Markdown | PR-comment summary: finding count per regulation and a table Location \| Article \| Evidence \| Description, with article links and the code evidence inlined (truncated to 120 chars). Findings only | Snapshot, rewritten atomically during the scan |
--report | JSONL | Chronological event ledger: one record per line ({"event", "ts", "payload"}) for every progress event (scan_start, file_start, orchestrator_done, file_done, file_error, scan_done, …) plus a full finding record per finding. Each line is flushed and fsynced | Append-only stream; never overwrites previous runs |
--log-file | Plain text | Timestamped human-readable log of the same events plus warnings and errors. If the file cannot be opened, the scan warns and continues | Appended; written by default to precommiteu_scan.log |
In short: --json-out is the final state document, --report is the audit trail of how the scan got there, --log-file is the operator log, and --sarif / --out are presentation formats for code-scanning UIs and PR comments respectively.
Never overwriting your files
precommitEU refuses to write over a file that is already there. If any path you pass to --json-out, --report, --sarif, --out or --log-file already exists, the scan stops with exit code 2 before a single model is loaded, and nothing on disk is touched:
$ precommiteu scan src/ --json-out result.json
error: --json-out target already exists: result.json
precommiteu never overwrites your files; pass a different path, delete it, or re-run with --force.
Three exceptions:
| Case | Behavior |
|---|---|
--force | Existing targets are overwritten |
--log-file left at its default | Never blocks a scan. precommiteu_scan.log is opened append-only. Pass --log-file explicitly and it is guarded |
--dry-run | Guard is skipped; a dry run writes no report files |
Snapshot reports (--json-out, --sarif, --out) are written by staging a <name>.precommiteu_tmp_<pid> sibling and atomically renaming it into place. A file of your own that happens to end in .tmp is never disturbed.
In CI, prefer a fresh path per run, or add --force:
precommiteu scan --ci --sarif findings.sarif --force
Progress on stderr is independent of the report files:
precommiteu scan src/ --progress jsonl # machine-readable progress on stderr
precommiteu scan src/ --progress none # silent
Findings vs advisories
A finding is a confirmed, evidence-backed result: the validator confirmed the candidate, cited a known article from the regulation pack, and quoted code evidence that is actually visible in the analyzed text. Findings drive the exit code (with --fail-on-findings) and appear in all report formats.
An advisory is an unconfirmed detector candidate: the detector flagged it, but validation did not confirm it. Advisories are informational, never affect the exit code, and are only emitted for files that produced no confirmed finding.
precommiteu scan src/ --show-advisories
Advisories (unconfirmed, non-blocking): 2
[gdpr] src/export.py: Bulk export of user records without consent check
[gdpr] src/jobs/cleanup.py: Retention period not enforced before deletion
When the regulation pack ships a case index (cases.jsonl), each advisory is additionally scored against known violation patterns. The annotation fields appear on the advisory in --json-out:
| Field | Meaning |
|---|---|
retrieval_verdict | Pattern-match verdict (e.g. violation_pattern) |
retrieval_confidence | Confidence of the verdict |
retrieval_similarity | Similarity to the closest known case |
retrieval_article_id | Article suggested by the matched cases |
An advisory whose verdict is violation_pattern at calibrated confidence and similarity thresholds, citing an article known to the pack, is promoted to a confirmed finding with source: "retrieval" and a description suffix noting the matched article and similarity. All other advisories stay non-blocking.
Analysis routing: --agent-mode
precommiteu scan src/ --agent-mode direct # fastest: fixed two-call scan per file
precommiteu scan src/ --agent-mode orchestrator # deepest: context-gathering agent per file
precommiteu scan src/ # auto (default)
| Mode | Behavior |
|---|---|
direct | Fixed detector + validator pass over the file’s chunks. No tool use. |
orchestrator | A context-gathering agent that can read neighboring files (sandboxed to the scanned file’s directory) and consult regulation texts before deciding. Bounded by --max-orchestrator-iterations and --max-wall-seconds-per-file. |
auto | direct, escalating to orchestrator per file only when the file references sibling files. |
Escalation in auto mode is purely structural: the scanner lists the other file stems in the same directory (stems of 3+ characters, first 200 siblings) and escalates when any sibling stem appears as a whole word anywhere in the file text. A file that never mentions a sibling file stays on the direct route.
Model selection
The scanner needs the precommitEU model bundle (see install.md): one shared base.gguf plus one detector-adapter.gguf per regulation.
<models-dir>/
base.gguf
gdpr/
detector-adapter.gguf
Point the scanner at it either way:
export PRECOMMITEU_MODELS_DIR=/opt/precommiteu/models
precommiteu scan src/
# or per invocation (overrides the env var):
precommiteu scan src/ --models-dir /opt/precommiteu/models
Resolution order: explicit file flags > --models-dir > $PRECOMMITEU_MODELS_DIR. Explicit paths bypass the directory layout entirely:
precommiteu scan src/ \
--orchestrator-model /opt/models/base.gguf \
--detector-adapter /opt/models/gdpr/detector-adapter.gguf
Failure behavior: a missing base model is a hard error (exit 2). A missing detector adapter is not: the scan continues on the base model alone and prints a ::warning:: marking the run as degraded mode.
Expert flags (not needed in normal operation):
# Pin a custom detector output grammar instead of the built-in one
precommiteu scan src/ --detector-grammar custom-detector.gbnf
The validator always runs on the base model with the built-in grammar and takes no adapter.
Performance tuning
# CPU-only host, pinned thread count
precommiteu scan src/ --gpu-layers 0 --threads 8
# Smaller context window for memory-constrained machines
precommiteu scan src/ --n-ctx 16384
# Tighter per-file budgets for large repos
precommiteu scan src/ --max-wall-seconds-per-file 45 --max-orchestrator-iterations 6
# Raise or remove the file-size cutoff (0 = no limit)
precommiteu scan src/ --max-file-bytes 0
| Flag | Effect |
|---|---|
--gpu-layers | Layers offloaded to GPU. 99 (default) offloads everything; 0 forces CPU-only inference. |
--threads | CPU inference threads. Default lets the runtime auto-select. |
--n-ctx | Context window in tokens (default 32768). Lower values reduce memory use. |
--max-file-bytes | Files above this size are skipped and reported in a files_oversized progress event (default 1000000; 0 disables the limit). |
--max-wall-seconds-per-file | Hard wall-clock budget per file (default 90 s). |
--max-orchestrator-iterations | Cap on agent steps per file in orchestrator routing (default 12). |
Exit codes
precommiteu scan src/ --fail-on-findings
echo $?
| Code | Meaning | | — | — | | 0 | Scan completed. Either no visible findings, or findings present but --fail-on-findings was not set. Also returned by --dry-run and bare precommiteu (help). | | 1 | --fail-on-findings was set and at least one confirmed finding remains after suppression filtering. Advisories and suppressed findings never trigger exit 1. | | 2 | Usage or configuration error: unknown regulation pack, no model paths configured, a required model/grammar file missing, an output target that already exists (see Never overwriting your files), --ci combined with positional paths, neither paths nor --ci given, or a git error in CI mode. | | 3 | Scan incomplete: one or more files could not be analyzed (model server failure, unreadable file, budget exhausted mid-analysis) and --fail-on-error was in effect (always on under --ci). The affected regulation’s status is failed in --json-out; a warning summarizing the errors is printed to stderr. | | 130 | Scan interrupted (Ctrl-C). Partial results are preserved in any --json-out / --sarif / --out files written so far. |
Suppressions
Three mechanisms, in increasing order of auditability:
| Mechanism | Effect | Leaves a record |
|---|---|---|
.eu-ignore file | Excludes paths during discovery, so they are never read | No |
Inline eu-ignore directives | Blanks source lines before the model sees them, preserving line numbers | No |
precommiteu-ignore: <article> reason="..." markers | Suppresses a confirmed finding and records your reason | Yes, in JSON and SARIF |
Suppressed findings are removed from console output and from the --fail-on-findings exit-code check. Only the marker form keeps an entry in --json-out and SARIF.
See Ignoring code and suppressing findings for the full syntax, matching rules and audit workflow.