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.


Back to top

Apache-2.0. A compliance signal, not legal advice.