Remediation

Fixing code

Review findings and fix them without leaving the terminal — deterministic edits applied for you, harder ones delegated to your coding agent, and a pull request at the end.

Fixing code

Run xgrep fix in a terminal and you get a review session over everything the last scan found: walk the findings, look at the fix before it happens, apply it, and open a pull request when you're done.

xgrep scan .      # find (writes the .xgrep findings cache)
xgrep fix         # review and fix
xgrep fix ./src   # or scan first, if there is no cache yet

Three kinds of fix

What xgrep does with a finding depends on how much the fix depends on your code.

Deterministic — there is exactly one correct edit and xgrep computes it from the match. It applies these itself.

Assisted — the right change depends on what the code is trying to do, so xgrep does not guess. It hands your coding agent everything it needs: the region to change, the strategy, the sanitizers it will accept, and what the result has to satisfy. The agent writes the edit; xgrep decides whether it counts.

Advisory — no edit, just guidance on what to change and why. Some things (a hardcoded secret, for instance) should not be "fixed" by an edit at all — the credential has to be rotated.

Every rule ships one of the three, so a finding always tells you what to do about it, even when nothing can be applied automatically.

In the review session

Walk findings with / (or click one). The detail pane shows the real before→after diff for a deterministic fix, or the plan for an assisted one.

keywhat it does
xfix this finding — applied, or delegated to the agent
space / * / enteradd to the batch / select all / fix the selected set
t f smark true-positive / false-positive / needs-review
ureset the verdict to unreviewed
gshow the finding's call neighborhood
Popen a pull request for what you've fixed
qquit (and offer the pull request)

Batching is the fast path on a long list: space the ones you believe, enter for a plan summary, confirm, and the whole set is fixed in one pass — each one still proven separately.

Opening a pull request

Once you have applied at least one fix, press P (or just quit — you get the same offer). xgrep creates a branch, stages only the files it changed, commits, pushes, and opens the request, while the agent writes the title and summary.

The host comes from your origin remote: GitHub via gh, GitLab via glab. Without the matching CLI — or if the push is rejected — it stops after the commit and tells you the branch name so you can open the request yourself.

You need push access and an authenticated CLI. Note that only files xgrep changed are staged, so unrelated uncommitted edits to those files ride along in the commit.

Delegating to a coding agent

xgrep never calls a model itself. It drives the coding-agent CLI you already runclaude by default, or codex, or your own command. The agent investigates with xgrep's own code intelligence, writes its edit, and xgrep re-scans to decide whether the finding actually cleared. Only then does it report the fix as applied.

xgrep fix --agent claude
xgrep fix --agent codex
xgrep fix --agent "my-agent --flag {prompt}"   # {prompt} marks where the prompt goes

The agent can also be set once, with XGREP_AGENT or agent.default in the config.

In CI, without a terminal

The same orchestration runs headless. --yes (and any non-interactive invocation) skips the review session:

# Triage the un-reviewed findings with the agent, then fix the confirmed ones.
xgrep fix --agent claude --triage-agent --yes

Scope what a run is allowed to touch with --rule (rule-ID allowlist), --min-confidence, --max-severity, and --confirmed. Anything out of scope is reported, never silently dropped. --strict exits non-zero on any rejection, so a pipeline can gate on fix failures.

How a fix is proven

Three gates, in order, on every edit:

  1. It still parses. The patched file is re-parsed; an edit that would introduce a syntax error into a clean file is rejected. xgrep never writes code it would refuse to report.
  2. It is written atomically. A temp file in the same directory is renamed over the target, preserving mode — there is no half-written state.
  3. It actually fixed the thing. The patched file is re-scanned, and the fix is rejected unless the finding is gone and nothing equal-or-worse appeared where it touched. (--no-rescan skips this; you rarely want to.)

fix verify runs the gates and shows you the verdict and diff without writing. fix apply writes what passes.

xgrep scan --json ./src | xgrep fix verify                     # preview every fix
xgrep scan --json ./src | xgrep fix apply --rule js-incorrect-suffix-check
xgrep scan ./src && xgrep fix apply                            # or use the cache

Each finding comes back with an outcome — applied, verified, rejected, needs-agent, advisory, out-of-scope, or stale-cache — so a script can tell what happened to every one.

Driving fixes from your own tooling

Everything above is available as data, if you are building your own remediation flow rather than using xgrep's.

scan --json carries each fixable finding's remediation under extra.fix_info: its kind (deterministic / assisted / advisory), the edits for a deterministic fix (byte spans plus line/column, so a renderer can place a multi-edit fix without re-reading the file), and for an assisted one the contract — the unsafe_region to change, the strategy, an optional canonical_fix, the recognized_sanitizers the re-scan will accept, and the acceptance_criteria. The same edits drive the SARIF fixes and the LSP quickfix.

To have xgrep verify an edit you authored, pipe a candidate in:

# Verify without writing — returns a verdict and a diff preview.
echo '{"path":"src/db.py","rule_id":"py-sql-injection","edits":[
  {"start_byte":420,"end_byte":480,"replacement":"..."}]}' \
  | xgrep fix verify -f rules.yaml

# Apply it, through the same gates.
echo '{…candidate…}' | xgrep fix apply -f rules.yaml

# Stream many, one NDJSON candidate per line.
cat candidates.ndjson | xgrep fix apply --batch -f rules.yaml

A fix spanning several files — add an import here, change the call site there — is one changeset: a files array of {path, edits}, applied all-or-nothing. No file is written unless every file passes.

echo '{"rule_id":"py-sql-injection","files":[
  {"path":"src/db.py",   "edits":[{"start_byte":420,"end_byte":480,"replacement":"…"}]},
  {"path":"src/util.py", "edits":[{"start_byte":0,"end_byte":0,"replacement":"import shlex\n"}]}
]}' | xgrep fix apply -f rules.yaml

A rejected candidate names its reasonspan-conflict, parse-error, finding-not-cleared, new-finding-introduced, or no-change — so you can iterate. The same two operations are exposed over MCP as fix_verify and fix_apply; see the AI agent guide.

Editors get the same signal: each LSP diagnostic's data payload carries fixKind alongside hasFix, so a client can offer a plain quickfix for a deterministic fix and an AI-assisted action for an assisted one.

Input precedence

fix takes findings from, in order: a scan --json report or a candidate piped on stdin, else the .xgrep/findings.json cache the last scan wrote. Point it at a path with no cache and it scans first.

On this page