Install
$ agentstack add skill-nerdy-krishna-securecoder-securecoder-fix ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ● Filesystem access Used
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
/securecoder-fix
You are running the /securecoder-fix skill. Your job is to safely remediate findings from a previous /securecoder-scan run, with every fix verified and any failed fix automatically rolled back.
> v0.7.0 scope. Handles both SAST findings (Semgrep, Bandit, Gitleaks, OSV-scanner) and compliance findings (ASVS v5 — produced by /securecoder-scan Phase B). Compliance findings with fix_complexity: "high" or lines: null are flagged manual_review_required rather than auto-fixed.
Two modes
- Default — apply fixes. The user invokes with no
--restoreflag. Skill reads the latest findings, asks which severities to fix, then runs the per-fix loop. - Restore — roll back. The user invokes
--restore(or asks in natural language "undo the last sccap-fix" / "restore run X"). Skill copies the run's backup files over the working tree and writes arestore_log.md. See [§ Restore](#restore-mode) below.
Pre-flight (apply-fix mode)
1. Locate the project root
- If a
.git/directory exists in the current working directory or any ancestor, use the git toplevel (git rev-parse --show-toplevel). - Otherwise, use the current working directory.
Capture it as PROJECT_ROOT.
2. Find the findings file to fix
By default, read /.securecoder/runs/latest/findings.jsonl. If the user asked for a specific run id ("fix run 20260514T140000Z"), use that run's findings.jsonl. If neither exists, fail with:
> No findings to fix. Run /securecoder-scan first.
Record the source run id as SOURCE_RUN_ID. Generate a new id for this fix run:
RUN_ID="$(date -u +%Y%m%dT%H%M%SZ)"
RUN_DIR="$PROJECT_ROOT/.securecoder/runs/$RUN_ID"
mkdir -p "$RUN_DIR/backups"
echo "$SOURCE_RUN_ID" > "$RUN_DIR/source_run.txt"
3. Load configuration
Read /.securecoder/config.json (or use defaults if missing). Capture: default_fix_scope, git.push_strategy, severity_floor.
4. Ask the user which severities to fix
Present a single-select picker with these options (pre-select config.default_fix_scope when available):
- All severities
- Critical only
- High only
- Medium only
- Low only
- Critical + High (Recommended)
- Critical + High + Medium
- Custom multi-select — agent asks "which severities?" and accepts any subset of
critical / high / medium / low / info - Interactive one-by-one — review each fix before it's applied
- By specific finding IDs — agent asks for a comma-separated list of canonical IDs
Record the chosen scope. Filter the findings list:
- Skip findings with
status: "suppressed"— they were marked as false positives via/securecoder-suppress, the HTML report's suppress UI, or.securecoder/suppressions.json. Record them in the fix log with statuseditor_skipped_suppressedso the user sees how many were skipped for this reason, distinct fromeditor_skipped(user manually skipped in interactive mode). - Of the remainder, keep
category: "sast"andcategory: "compliance"findings whoseseverityis in the scope. - For findings with
fix_complexity: "high"orlines: null, append to a separateMANUAL_REVIEWlist (they're recorded in the fix log with statusmanual_review_requiredbut never auto-fixed). - Everything else lands in
TO_FIX.
Capture all three lists (TO_FIX, MANUAL_REVIEW, SUPPRESSED_SKIPPED).
If TO_FIX is empty, print "No findings match the selected scope. Exiting." and exit cleanly.
5. Git clean-tree check
if [ -d "$PROJECT_ROOT/.git" ]; then
if ! git -C "$PROJECT_ROOT" diff --quiet || ! git -C "$PROJECT_ROOT" diff --cached --quiet; then
# Uncommitted changes exist
# Ask user: stash and continue / abort / proceed anyway (risky)
:
fi
fi
If the user picks abort, exit cleanly. If stash, run git -C "$PROJECT_ROOT" stash push -u -m "securecoder-fix auto-stash before $RUN_ID" and remember to restore it post-flight. If proceed anyway, log a warning to $RUN_DIR/log.md.
For non-git repos, skip this check but mention: "No git repo detected — backups will be used for rollback if needed."
6. Protected branch check
BRANCH="$(git -C "$PROJECT_ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo no-git)"
case "$BRANCH" in
main|master|release/*|prod|production)
PROTECTED=1 ;;
*)
PROTECTED=0 ;;
esac
If PROTECTED=1, ask:
> You're on a protected branch (`). Create securecoder-fix/` branch before applying fixes?
Default yes. On approval: git -C "$PROJECT_ROOT" checkout -b "securecoder-fix/$RUN_ID".
7. Backup capture
For every distinct file path in TO_FIX, copy it to $RUN_DIR/backups/ before any edit happens:
for file in ; do
src="$PROJECT_ROOT/$file"
dst="$RUN_DIR/backups/$file"
mkdir -p "$(dirname "$dst")"
cp "$src" "$dst"
done
Backups exist independently of git history. If git rollback fails or the repo isn't git, backups are the ground truth.
8. Cost estimate
Fix run estimate:
Findings to fix:
Backed up files:
Est. token cost: ~ input + ~ output tokens
Approximate cost at common rates:
Claude Opus 4.7: $
Claude Sonnet 4.6: $
Claude Haiku 4.5: $
Continue? [proceed / abort]
(Use $15/$75 per M tokens for Opus, $3/$15 for Sonnet, $1/$5 for Haiku, in/out respectively.)
Wait for proceed. On abort, append cancelled-at-estimate to $RUN_DIR/log.md and exit cleanly.
Per-fix loop
For each finding in TO_FIX:
F.1 Show the user what's being fixed (interactive mode only)
If the user selected interactive one-by-one, display the finding summary before doing any LLM work:
[/] · · :L-
Rule:
CWE:
Remediation hint:
Action? [apply / skip / suppress / quit]
apply→ proceed with F.2.skip→ mark findingeditor_skippedand continue to the next finding.suppress→ mark this finding as a false positive. Ask the user for a one-line reason, then invoke/securecoder-suppress add --match '{"id": ""}' --reason ""(the agent runssuppress.pydirectly). Mark the findingeditor_skipped_suppressedin the fix log. Continue to the next finding. The suppression is now recorded in.securecoder/suppressions.jsonand will apply to future scans automatically.quit→ exit the loop and proceed to post-flight.
F.2 Compose the LLM prompt
Prompt template:
> You are fixing a security finding in a codebase. Produce a minimal patch that resolves the vulnerability while preserving behavior. > > Finding: > - File: ` > - Lines: - > - Severity: > - Rule: > - CWE: > - Title: > - Description: > - Evidence: ```` > - Remediation hint: > > **Current file content** (with line numbers, ±10 lines of context around the finding): > ```` > > Produce one or more SEARCH/REPLACE blocks in EXACTLY this format. The SEARCH section must match the existing file content byte-for-byte (whitespace included). The REPLACE section is the corrected version. Do not include any other text between blocks. > > ` > > ======= > > >>>>>>> REPLACE > ` > > Rules: > 1. Make the SMALLEST change that resolves the vulnerability. > 2. Preserve indentation, blank lines, and comments unless they are part of the vulnerability. > 3. If you cannot fix this without architectural changes, say so explicitly in plain text and produce no SEARCH/REPLACE blocks. The skill will mark the finding editor_failed` with your reason.
F.3 Save the LLM response and run the patch applier
Write the LLM response to a temp file:
PATCH_FILE="$RUN_DIR/_patches/$(printf '%04d' $finding_index)_$finding_id_short.patch"
mkdir -p "$RUN_DIR/_patches"
echo "$llm_response" > "$PATCH_FILE"
Run the applier:
python3 "/scripts/apply_patch.py" "$PROJECT_ROOT/$file" --patch "$PATCH_FILE" --json
Capture the JSON status. Interpret:
status: "ok"→ patch applied; proceed to F.4.status: "no_match","multiple_match","no_blocks"→ patch failed to apply. Restore from backup if file changed, increment retry count, and either retry F.2 with retry context (up to 3 total tries) or markeditor_failed.
F.4 Syntax check the patched file
python3 "/scripts/syntax_check.py" "$PROJECT_ROOT/$file" --json
Exit 0 = clean; non-zero = syntax error introduced. On error, restore from backup and trigger a retry with the syntax-error message in the retry context.
F.5 Re-scan to verify the fix
Best-effort verification that the finding is gone and no NEW finding of equal-or-higher severity was introduced. The mechanism depends on category:
F.5.sast — for SAST findings
Re-run the originating tool on just the fixed file:
case "$source" in
semgrep)
"$SEMGREP_BIN" --metrics=off --quiet --json --output "$RUN_DIR/_recheck.json" \
--config "$RULES_DIR/" "$PROJECT_ROOT/$file" 2>/dev/null || true
;;
bandit)
"$BANDIT_BIN" -f json -o "$RUN_DIR/_recheck.json" "$PROJECT_ROOT/$file" 2>/dev/null || true
;;
gitleaks)
"$GITLEAKS_BIN" detect --no-banner --report-format json \
--report-path "$RUN_DIR/_recheck.json" --source "$PROJECT_ROOT/$file" \
--no-git --exit-code 0 2>/dev/null || true
;;
osv-scanner)
"$OSV_BIN" --format json --output "$RUN_DIR/_recheck.json" "$PROJECT_ROOT/$file" \
2>/dev/null || true
;;
esac
Normalize via the same normalize_.py from /securecoder-scan/scripts/. Compare to the original finding:
- The finding's canonical ID should NOT be in the re-scan results. If it is, the fix didn't actually resolve the issue. Restore from backup and retry.
- No NEW findings of equal-or-higher severity at the same file should appear. If one does, the fix introduced a different vulnerability. Restore from backup and retry.
F.5.compliance — for compliance findings
Re-run the architect prompt for the originating chapter on the fixed file. The chapter ID is in the finding's tags (e.g., "V1"). The framework markdown is already cached at ~/.cache/securecoder/rules/frameworks/asvs//5.0/en/ from the original scan.
Compose the same architect prompt the scan used, but on the now-patched file. Save the response to $RUN_DIR/_recheck_compliance/__.md. Validate coverage matrix (one retry on incomplete). Normalize via normalize_compliance.py.
Compare to the original finding:
- The original finding's
source_rule_id(control ID) should NOT be present in the re-scan's findings. If it is, the fix didn't resolve the failing control. Restore from backup and retry. - No NEW compliance findings of equal-or-higher severity at the same file × chapter should appear. If one does, the fix introduced a different compliance failure. Restore from backup and retry.
If the re-scan LLM call itself fails (3 tries), mark the finding applied_unverified — the patch is applied, the file is left in its post-fix state, but verification couldn't complete. The post-flight summary will flag these separately so the user can spot-check.
If verification passes, proceed to F.6.
F.6 Commit the fix
Skip this step for non-git repos.
Commit message format differs by category:
- SAST:
fix(securecoder): / [] - Compliance:
fix(securecoder): / [compliance / ]
SHORT_ID="${finding_id:0:8}"
if [ "$category" = "compliance" ]; then
SUBJECT="fix(securecoder): $severity/$title [compliance $source/$source_rule_id $SHORT_ID]"
else
SUBJECT="fix(securecoder): $severity/$title [$SHORT_ID]"
fi
COMMIT_MSG="$SUBJECT
Source: $source
Category: $category
Rule: $source_rule_id
CWE: $cwe_csv
Original lines: $file:L$start-L$end
Remediation: $remediation_hint
Applied by /securecoder-fix in run $RUN_ID."
git -C "$PROJECT_ROOT" add "$file"
git -C "$PROJECT_ROOT" commit -m "$COMMIT_MSG" >/dev/null
F.7 Push (per config.git.push_strategy)
push-each:git -C "$PROJECT_ROOT" pushimmediately.commit-local-push-at-end: accumulate; push once in post-flight.commit-local-never-push: skip.
F.8 Update the finding status
Append to $RUN_DIR/fix_log.jsonl:
{"id": "", "status": "applied", "commit_sha": "", "tries": }
F.9 Retry semantics
On any failure (parse, syntax, re-scan), restore from backup and retry the per-fix loop from F.2 with retry context appended to the LLM prompt:
> ## Retry context (try of 3) > Your previous attempt failed: . > > > > Produce a corrected SEARCH/REPLACE block. Follow all the original rules.
Maximum 3 tries per finding. After 3 failures, mark editor_failed with the failure reason, leave the file in its restored (pre-attempt) state, and move on.
Post-flight
P.1 Push accumulated commits (if commit-local-push-at-end)
git -C "$PROJECT_ROOT" push origin HEAD 2>&1 || true
P.2 Restore stash (if pre-flight stashed)
git -C "$PROJECT_ROOT" stash pop || true
P.3 Write a manifest
python3 -
Editor failed: (see $RUN_DIR/fix_log.jsonl for reasons)
Manual review required: (compliance findings or fix_complexity=high)
Skipped: (interactive mode only)
Git diff: git diff $SOURCE_RUN_ID..HEAD (or just `git diff --stat`)
To roll back this fix run:
/securecoder-fix --restore $RUN_ID
Backups for this run: .securecoder/runs/$RUN_ID/backups/
P.5 Update latest pointer
The fix run dir doesn't replace the scan run dir's latest. latest continues to point at the most recent scan run; the fix run's existence is referenced via the commit history and the source_run.txt file.
Restore mode
When invoked with --restore or via natural-language ("undo run X" / "restore the fix from earlier"):
R.1 Locate the run
RESTORE_RUN_DIR="$PROJECT_ROOT/.securecoder/runs/$RUN_ID"
[ -d "$RESTORE_RUN_DIR/backups" ] || fail "No backups found for run $RUN_ID."
R.2 Show what would be restored
For each file in $RESTORE_RUN_DIR/backups/:
diff -u "$PROJECT_ROOT/" "$RESTORE_RUN_DIR/backups/" || true
Group the diffs and present them to the user. Highlight:
- Files in backup that no longer exist in working tree (deleted since fix) → ask if user wants to recreate.
- Files in backup whose current content has been modified since the fix landed → diff shown; user confirms per-file or for the whole batch.
R.3 Confirm restoration
> Restoring will overwrite files in the working tree with their pre-fix backups. of these have been modified since the fix. > > Proceed with restore? [yes / abort / per-file]
R.4 Apply the restore
while IFS= read -r path; do
rel="${path#$RESTORE_RUN_DIR/backups/}"
cp "$RESTORE_RUN_DIR/backups/$rel" "$PROJECT_ROOT/$rel"
done Revert the corresponding git commits too? [yes / no]
On yes, find commits referencing this run ID and revert them:
```bash
git -C "$PROJECT_ROOT" log --grep "$RUN_ID" --format="%H" | while read sha; do
git -C "$PROJECT_ROOT" revert --no-commit "$sha"
done
git -C "$PROJECT_ROOT" commit -m "Revert securecoder-fix run $RUN_ID"
R.6 Write the restore log
cat > "$RESTORE_RUN_DIR/restore_log.md"
- Modified-since-fix files restored:
- Git revert performed:
## Restored files
$(find "$RESTORE_RUN_DIR/backups" -type f -exec echo " - {}" \;)
EOF
Failure handling
Soft failures — log and continue.
- One finding fails after 3 retries → mark
editor_failed, restore from backup, move on. - LLM emits no SEARCH/REPLACE blocks → mark
editor_failedwith reason "model declined or produced no patch". - Re-scan tool crashes → log the crash, accept the apply (best effort), mark status
applied_unverified.
Hard failures — write a crash report and exit.
- Cannot write to
$RUN_DIR(disk full, permissions). - Cached SAST tool binary is missing AND a new install fails.
- User attempts
--restoreon a run id with nobackups/dir.
On hard failure, write $RUN_DIR/crash_report.md and exit. Do not partially write or partially commit.
Invariants
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: nerdy-krishna
- Source: nerdy-krishna/securecoder
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.