Agentic Remediation
How findings get fixed in Gadriel Code — from triaging a single finding at
the terminal, through the MCP fix_finding round-trip an AI assistant runs
mid-session, up to autonomous remediation campaigns that fix a whole class
of findings repo-wide.
Every workflow on this page shares one invariant: Gadriel proposes, the host applies. Gadriel never writes source code. It emits a diff + rationale; the developer (or the host AI agent) applies it, and Gadriel re-scans to verify. Every action is appended to the local audit log.
Related: AI-coding integration · MCP tools reference · Tiered analysis model · False positives · Privacy boundary · CLI reference
1. Three ways to remediate
| Workflow | Driver | Scale | LLM | Source of truth |
|---|---|---|---|---|
| Single-finding triage | gadriel code fix <ID> (CLI) | one finding | optional | terminal + audit log |
| MCP fix round-trip | fix_finding tool | one finding | yes | host agent + audit log |
| Remediation campaign | start_remediation_campaign + campaign_advance | repo-wide class | yes | .security/campaigns/ |
All three are grounded on the same deterministic findings in
.security/findings.json (Tier 0 — see tiers.md), and all three
route through the per-pillar reasoning runner when they call an LLM.
2. Single-finding triage — gadriel code fix <FINDING_ID>
The terminal path. Given a finding ID, it does one of
three things depending on flags. See the
fix command in the CLI reference.
bash# accept the finding as real and get a confirmation + fix suggestiongadriel code fix CODE-W1-L2-021# mark it a false positive (records a durable waiver + Bayesian feedback)gadriel code fix CODE-W1-L2-021 --false-positive \--reason "zero-copy idiom, not an interface cast"# affirm it is a real risk (bumps the posterior toward "real")gadriel code fix CODE-W1-L2-021 --confirm-real
The finding argument accepts <ID> or <ID>#<SCAN_SEQUENCE>; the #<seq>
suffix disambiguates when the same rule fired at multiple sites. With no
suffix and multiple matches, fix lists them and refuses to guess.
2.1 The three dispositions
Confirm real (no flag) — hand it to an agent. fix loads the finding,
routes it to the right per-pillar agent via AgentDispatcher, retrieves the
top-1 relevant skill, builds the prompt, and invokes the reasoning runner. It
prints the verdict (confirmation, explanation, rationale, proposed fix,
compliance controls, cost/latency) and appends audit-log entries: a
confirmation event plus a fix_proposed event carrying the pre-fix
before_hash. The fix is printed, not applied — the developer (or their
editor agent) applies the suggested diff.
--false-positive — waive it. Short-circuits the LLM entirely
(zero LLM calls). It:
- writes a durable
[[waiver]]to.gadriel-waivers.toml, keyed by(rule_id, path, line), which segregates the finding on every future scan regardless of re-detection count (the--reasontext is recorded verbatim); - updates the per-repo Bayesian posterior with a full-weight
DismissNotRealsignal (β += 1.0) — the CLI caller is a trusted human; - records a replay-buffer episode and transitions any still-
proposedfix rows torejected; - appends a
confirmed_false_positiveaudit event (agent_runner: "human").
See false-positives.md for how waivers and priors compound.
--confirm-real — affirm it. Also LLM-free: bumps the posterior toward
"real" (α += 1.0), records a lower-priority replay episode, and appends a
confirmed_real audit event. It writes no waiver.
2.2 The source-consent gate
When the live ClaudeRunner path is active (i.e. GADRIEL_USE_MOCK_RUNNER
unset), the confirm-real path ships a ~30-line source excerpt around the
finding to Anthropic. fix surfaces this before touching the network and
requires explicit --send-source consent; without it, fix
aborts. Consent may also be persisted via [tier2].source_consent in
.gadriel.toml, and a config-load failure fails closed (consent stays
whatever the flag said). Mock runs never reach the network and ignore the
flag. See privacy.md.
3. The MCP fix round-trip — fix_finding
The in-session path an AI coding assistant runs. Instead of a human reading a
report and relaying it, the assistant calls the fix_finding MCP tool, gets
back a diff, applies it, and asks Gadriel to re-validate. This is the loop
described in ai-coding-integration.md §3.
assistant edits code
│
▼
validate_buffer / validate_file ──▶ findings?
│
└─ findings ──▶ fix_finding { finding_id, runner_id: "claude" }
│
▼
FixProposal { diff, rationale, … } ← Gadriel proposes
│
host agent APPLIES the diff ← host applies (out of process)
│
▼
validate_file (re-scan) ──▶ finding cleared?
│
└─▶ .security/audit-log.jsonl (local)
fix_finding returns a FixProposal envelope
({ status, runner_id, finding_id, diff?, rationale, iterations_used, tool_calls })
when a runner_id is supplied. The host agent applies the returned diff —
fix_finding itself writes no source. Re-validation is a fresh
validate_file (or validate_buffer) call; a cleared finding closes the
loop. Because validate_buffer runs on the unsaved buffer, a vulnerability
can be corrected before the file is ever written — so it never enters git.
For the exact params, response shapes, error envelopes, and the runner-level
source-consent enforcement, see the
fix_finding entry in the MCP tools reference.
4. Autonomous remediation campaigns
A single fix_finding call fixes one finding. To fix a whole class at
repo-scale — "fix all 40 confirmed SQLi findings" — without the host agent
hand-rolling a 40× loop, Gadriel exposes a stateful two-tool pair:
start_remediation_campaign and campaign_advance. The host still applies
every diff; Gadriel drives the ordering, verification, and progress accounting.
4.1 Starting a campaign
start_remediation_campaign loads .security/findings.json, filters it
(severity / pillar / scan_type / rule_id / confidence_tier, plus an
optional finding_ids allowlist), orders the worklist (highest
effective_risk first, then attack-path presence, then shortest path;
same-file findings stay adjacent), persists state to
.security/campaigns/<campaign_id>.json, and takes a single-campaign-per-repo
.lock. It returns { campaign_id, total, first_batch }.
Only one campaign runs per repo at a time. A crashed campaign's lock becomes
stale after an hour and is reclaimed automatically; an operator can also force
past a live lock with force: true.
4.2 The advance loop
campaign_advance is the fix → apply → verify → next step the host calls
repeatedly:
┌──────────────────────────────────────────────┐
campaign_advance ───▶│ pick next Pending item → mark InProgress │
(applied: absent) │ propose diff via fix_finding (LLM) │
│ return { status: "in_progress", proposed_fix }│
└──────────────────────────────────────────────┘
│
host APPLIES the diff (out of process)
│
┌──────────────────────────────────────────────┐
campaign_advance ───▶│ re-scan the touched file │
(applied: true) │ → verified / verified_partial / regressed │
│ then advance to the next Pending item │
└──────────────────────────────────────────────┘
│
… repeat until no Pending items …
│
┌──────────────────────────────────────────────┐
│ { status: "campaign_complete", summary } │
└──────────────────────────────────────────────┘
- First call for an item (no
appliedsignal) — reads the nextpendingitem, marks itin_progress, callsfix_finding's propose logic, and returns a diff + rationale. - The host applies the diff, then calls again with
applied: true. - Verification call — runs the re-scan on the touched file, records a terminal status, and returns the next item (or the completion summary).
An explicit decline (applied: false) writes a mechanized waiver and moves
the item to waived rather than leaving it stuck in_progress.
4.3 Per-item terminal states
| Status | Meaning |
|---|---|
verified | Re-scan confirmed the finding is gone (Secrets, or SAST on Python). |
verified_partial | Fix applied but the finding's scan type is outside instant-re-scan coverage (SCA / Container / API / non-Python SAST), or the re-scan itself failed. Never a silent synonym for verified. |
regressed | The finding is still present, or a new one was introduced — never waived, never dropped, always surfaced in the summary. |
waived | Runner could not fix it, or the host declined the diff → mechanized durable waiver with an auto-populated reason. |
skipped_no_agent | No agent/runner available (an unauthored pillar — only Security and Safety agents exist today; the other 6 fail closed — or a stub/offline runner). |
fixed | Host reported applied: true but the finding could no longer be re-resolved to attempt verification (rare edge case). |
Verification is a line-based match (rule/pattern id + line_start), not a
full source diff — good enough for an in-place fix at the flagged line, but a
diff that shifts surrounding lines can produce a false regressed/verified
read. The completion summary buckets items by status, lists every
regressed finding explicitly, and — when any item is verified_partial —
carries a verified_partial_note spelling out exactly what could not be
deterministically re-verified (anti-overclaim discipline).
4.4 Campaign state on disk
State lives under .security/campaigns/:
.security/campaigns/
├── .lock # single-campaign-per-repo lock (holder id + mtime)
└── campaign-1719….json # the campaign worklist + running cost
The state file's JSON shape:
json{"campaign_id": "campaign-1719…","items": [{ "finding_id": "CODE-W1-AI-001", "status": "verified" },{ "finding_id": "CODE-W1-AI-002", "status": "in_progress" }],"cost_usd": 0.42}
The only files a campaign writes are this state file, the campaigns
directory, the .lock, and (on declines/failures) .gadriel-waivers.toml.
Byte-for-byte, the repo tree outside .security/ is unchanged across a full
campaign run — asserted by the module's own tests.
5. Safety invariants
The remediation surface is deliberately conservative. These hold across every workflow above:
- Gadriel never writes source.
fix_findingandcampaign_advancereturn diffs; the host agent applies them out of process and self-reportsapplied: true. No auto-commit, no auto-git add. A buggy or malicious diff can never be silently committed by Gadriel. - Every action is audited. Confirmations, proposed fixes, dismissals, and
campaign outcomes append to
.security/audit-log.jsonl(and durable dispositions to.gadriel-waivers.toml). The audit log is local-only compliance evidence — see privacy.md. - Verification never overclaims. Instant re-scan is deterministic only
for Secrets + Python-SAST; everything else is
verified_partial, and a regression is always surfaced, never waived or dropped. - The gate stays deterministic. Remediation reads and clears Tier-0 findings; it cannot promote an AI/Tier-2 signal into the gate. Tier 2 is opt-in and default off. See tiers.md.
- Source leaves the machine only with consent. Live LLM fixes require
explicit source-content consent (
--send-sourcefor the CLI;GADRIEL_MCP_SOURCE_CONSENT=truefor the MCP server); the gate fails closed.
5.1 Documented TODO — the per-fix Tier-2 second-opinion gate
An optional per-fix Tier-2 second-opinion gate is planned: a
corroboration step that would run before a campaign item's re-scan verification
and narrow the verified_partial bucket further. It is not wired yet.
campaign_advance today runs re-scan verification directly after applied: true
with no corroboration step first. When it does land it inherits the never-gate invariant — a
negative second opinion would flag and demote, never hard-fail the campaign or
auto-revert an applied fix.
