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 yetThree 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.
| key | what it does |
|---|---|
x | fix this finding — applied, or delegated to the agent |
space / * / enter | add to the batch / select all / fix the selected set |
t f s | mark true-positive / false-positive / needs-review |
u | reset the verdict to unreviewed |
g | show the finding's call neighborhood |
P | open a pull request for what you've fixed |
q | quit (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
run — claude 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 goesThe 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 --yesScope 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:
- 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.
- It is written atomically. A temp file in the same directory is renamed over the target, preserving mode — there is no half-written state.
- 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-rescanskips 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 cacheEach 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.yamlA 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.yamlA rejected candidate names its reason — span-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.
Triage
Decide which findings are real before you change anything — by hand in the review session, or headlessly with a coding agent — and fix only what someone stood behind.
Fixing dependencies
Upgrade the packages a CVE names — direct and transitive — with the package manager you already use, verified by re-detecting the vulnerability afterwards.