Skip to content

Seki — verification-ladder remediation (security-patcher)

Seki's remediation phase turns each confirmed finding into a verified diff and (optionally) commits it. It is off by default (Seki is a read-only SAST auditor out of the box); opt in with --var remediate=true and choose one of three modes via --var remediation_mode (default apply_gated).

The shape was adapted from Anthropic's defending-code-reference-harness/patch reference (build → reproduce → regress → re-attack → reviewer). Published evals on the harness pipeline show ~60% of model patches build + remove the original scanner symptom, but <15% survive the adversarial re-attack rung — i.e. the patches that look fine on tier-0 and tier-1 still leave the root cause reachable somewhere else in the package. Seki's ladder exists to catch that 45% before merging.

The phase is wired downstream of update_file_records and runs at most --var max_fix_per_run findings per run (default 10). Crypto + secrets findings are filtered out before the loop ever runs (--var hard_stop_categories="crypto,secrets") and surfaced as human-review board issues; see crypto-handling.md.

The ladder — what each rung proves

For one finding, the loop body is:

RungWhat it provesFailure status
build_rungPatched tree still compilesrejected (build_failed)
reproduce_rungOriginal scanner+matcher no longer fires on the patched filerejected (reproduce_still_hits) or uncertain when no oracle exists for the scanner
regress_rungPre-existing test suite still passesrejected (tests_failed)
reattackThe same vuln CLASS is not reachable elsewhere in the packagerejected (reattack_found_variants)
reviewer_isolationThe diff is a defensible fix judged from {file, line, category, diff} alone (prompt-injection-resistant)rejected (reviewer_rejected) or uncertain when any risk_flags[] are raised

Status truth table (after the ladder):

buildreproduceregressreattackreviewerrisk_flagsoracle?status
okgonepassedheldapprovedemptyyesverified
okgonepassedheldapprovednon-emptyanyuncertain
okgonepassedheldapprovedemptyno oracle (after_hits == -1)uncertain
anyanyanyanyany(any)anyrejected on first failing rung

Reviewer-isolation contract

The reviewer is the LAST rung and the strongest defence against prompt injection that rode in through scanner messages, source comments, or the patch author's own rationale. It sees a schema-projected input — the project_review_input compute publishes only {file, line, category, diff} into review_isolation_input, and the reviewer's input schema does not carry any of the upstream scanner_body / exploit_hypothesis / patch_author.rationale fields. The isolation is enforced at the schema level, not by prompt discipline.

The reviewer also runs:

  • tools: [read_file, glob, grep]no bash, so it cannot run scanners, cannot read scanner output indirectly, and cannot git log/git blame (which leaks commit messages that carry the same injection surface).
  • readonly: true — no write tools.
  • session: fresh — zero context bleed from patch_author.

It MAY open the cited source file + its immediate neighbours. It MUST NOT read anything under .sec-audit/ (nor the out-of-tree scanner output under scan_dir). Full policy + the canonical risk_flags[] vocabulary (touches_crypto_primitive, removes_existing_guard, widens_attack_surface, touches_iterion_state, comment_only, regression_test_only, cross_file_drive_by, unfamiliar_pattern) lives in reviewer-isolation.md.

Crypto + secrets hard-stop

Two finding categories are never auto-patched, regardless of mode or ladder outcome:

  • crypto — silent-failure-mode bugs (weakened MAC, non-constant-time compare, re-used IV, downgraded RNG, hash-then-MAC vs encrypt-then-MAC). Scanners catch patterns, not protocol correctness; the reattack rung has no oracle for constant-time leaks; a patch that compiles and re-passes the unit test can be silently broken everywhere else.
  • secrets — rotations are operator-only, not diffs.

Defence-in-depth: remediation_plan filters vars.hard_stop_categories ("crypto,secrets" by default) into hard_stopped[] before the loop ever runs, AND the patch_author prompt's CRYPTO/SECRETS HARD-STOP clause refuses to author a fix when the finding's finding_type is in the list OR the file path matches the crypto path patterns in crypto-handling.md (**/crypto/**, **/jwt/**, **/keys/**, **/x509/**, …).

The board issue stays open with labels seki-hard-stop:<finding_type> + human-review, and remediation_report writes a row in REMEDIATION.md's Hard-stopped section pointing the operator at the rotation runbook.

The three modes

--var remediation_mode controls how the workflow disposes of verified fixes once the ladder has cleared them.

ModeVerified diff →Rejected / uncertain →Pause?User branch touched?
proposeDiff artifact under <patch_dir>/<finding_id>-verified.diff, working tree revertedDiff artifact (*-rejected.diff / *-uncertain.diff), working tree revertednonever
apply_gated (DEFAULT)Committed on temp branch iterion/sec-fix/<run-id>Reverted (so a rejected attempt does not pollute the temp branch); artifact still writtenYES on approve_fixes after the looponly if operator answers approved=true (git merge --no-ff)
apply_autoCommitted on temp branch AND merged back without a gateReverted; artifact writtennoalways (after the loop, unless zero verifieds landed)

In all three modes the diff artifact is written to <patch_dir>/<finding_id>-<status>.diff for the audit trail (vars.patch_dir = .sec-audit/patches by default), the kanban board is labelled per the mode-specific table in remediation_report's system prompt, and the working tree is restored to a clean state before the next finding starts.

When to use which mode

ModeUse whenDon't use when
proposeFirst runs on a new repo, demos, dry-runs, hostile-PR review where you don't want any branch mutationYou expect to apply most of the fixes — you'll re-run with apply_gated anyway
apply_gatedDefault for human-in-the-loop dev / PR workflows. Lets you inspect the temp branch + REMEDIATION.md before decidingA trusted CI pipeline that already has a downstream PR review gate
apply_autoTrusted CI pipeline with its own downstream review (the merged PR is reviewed elsewhere)Local dev (the human is right there — let them gate)

apply_gated is the safe default: even if Seki produces a wrong patch, no fix lands on your branch without your approved=true. The runtime suspends budget on the human pause so a long review wait costs nothing.

Temp-branch + human-gate + merge flow (apply_gated)

apply_auto shortcuts the fix_summaryapprove_fixes pause and routes straight from apply_router to merge_fixes. propose skips prepare_branch (no temp branch created), so the record_finding node falls back to its revert-only path even for verified outcomes, and the exit_router lands directly on remediation_report.

Inspection before answering the gate

When the workflow pauses on approve_fixes, inspect the temp branch:

bash
git -C <workspace> diff <original_branch>..iterion/sec-fix/<run-id-suffix>
git -C <workspace> log  <original_branch>..iterion/sec-fix/<run-id-suffix> --oneline
ls .sec-audit/patches/
cat .sec-audit/patches/REMEDIATION.md

REMEDIATION.md is written by fix_summary BEFORE the pause and embedded into the approve_fixes_instructions prompt — so even a "resume blind" answer has the counts (verified / uncertain / rejected / hard-stopped), the temp/original branch names, and the resume commands visible.

Resume

bash
# Merge verified commits into the original branch (--no-ff)
iterion resume --run-id <id> --answer approved=true

# Abandon: working tree returns to original_branch, temp branch is KEPT
# for inspection. You can cherry-pick a subset later or `git branch -D`
# the temp branch when done.
iterion resume --run-id <id> --answer approved=false

After resume, remediation_report re-runs:

  • approved=true → merged into original, board labels patched:seki + seki-verdict:verified + seki-merge-sha:<sha>.
  • approved=false → abandoned, board labels seki-fix-pending + patch-proposed:seki + seki-temp-branch:<branch>.

A pre-existing dirty working tree is stashed by prepare_branch (stashed: true, stash_ref recorded) and git stash pop is run on the original branch by either merge_fixes or abandon_fixes — user WIP is never silently dropped.

End-to-end example — Claude Code subscription, no API key

bash
ITERION_SEC_AUDIT_BACKEND=claude_code \
ITERION_SEC_AUDIT_MODEL=claude-opus-5 \
ITERION_SEC_AUDIT_VOTER_V1_MODEL=claude-opus-5 \
ITERION_SEC_AUDIT_VOTER_V3_MODEL=claude-opus-5 \
ITERION_REFERENCES_ROOT=$HOME/lab/ai/references \
  devbox run -- iterion run bots/sec-audit-source/main.bot \
    --var workspace_dir=$(pwd) \
    --var diff_base=origin/main \
    --var enable_deepsec=true \
    --var remediation_mode=apply_gated \
    --var max_fix_per_run=5

# Workflow runs through scanners, 3-voter triage, then enters
# remediation. On pause:

iterion resume --run-id <id> --answer approved=true
# → merge --no-ff iterion/sec-fix/<run-id> into your current branch
# → REMEDIATION.md updated with the merge sha
# → board issues labelled patched:seki

# Or to keep the temp branch but not merge:
iterion resume --run-id <id> --answer approved=false
# → checkout your original branch, temp branch kept
# → board issues labelled seki-fix-pending

A propose-only dry run uses --var remediation_mode=propose — every verified diff lands as a *-verified.diff artifact under .sec-audit/patches/ and the working tree returns to where it started after every finding. Useful when you want to read the diffs before deciding to re-run with apply_gated.

Knobs summary

VarDefaultNotes
remediatefalseOpt-in; false → phase fully skipped, identical to pre-remediation behaviour. Set true to enable.
remediation_mode"apply_gated"propose | apply_gated | apply_auto
hard_stop_categories"crypto,secrets"Comma-separated finding_type values that bypass the loop entirely
max_fix_per_run10Loop budget — bounds the per-run patch attempts
patch_attempts1Inner per-finding retry budget; future nested-retry hook
patch_dir"${PROJECT_DIR}/.sec-audit/patches"Diff artifacts + REMEDIATION.md land here
ITERION_SEC_PATCH_MODELclaude-opus-5patch_author, reattack, reviewer_isolation, remediation_report model
ITERION_SEC_PATCH_EFFORT_*(per node)_AUTHOR=high / _REATTACK=high / _REVIEW=high / _REPORT=medium — lower to claw cost

See also