Install
$ agentstack add skill-air-gapped-skills-patch ✓ 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 No
- ● Shell / process execution Used
- ✓ Environment & secrets No
- ✓ 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
patch
Third leg of the static pipeline (/vuln-scan → /triage → /patch). Turns a ranked list of verified findings into candidate diffs.
The skill never applies a diff to the target repo. Output is inert text in ./PATCHES/ for a human to review and apply out-of-band — see § "Reviewing generated patches" at the end of this file. There is no --apply or --approve flag by design: the capability isn't present, so it can't be prompt-injected into use.
Invoke with /patch [--repo PATH] [--top N] [--id fNNN] [--model M] [--fresh].
Arguments (parse from $ARGUMENTS):
- findings path (first positional, required):
TRIAGE.json,
VULN-FINDINGS.json, a pipeline results/// directory, or any JSON the /triage ingest table recognizes.
--repo PATH: target codebase, read-only (default cwd). Required for
static mode; the skill stops if cited files don't resolve under it.
--top N: patch only the N highest-severity true positives (static mode).--id fNNN: patch only the finding with this id.--model M: passed through tovuln-pipeline patchin execution-verified
mode. Ignored in static mode (subagents inherit the orchestrator's model).
--fresh: ignore./.patch-state/checkpoint and start over.
Tools. Prefer Read, Glob, Grep, Write, Task. Some sessions do not provision Glob or Grep; allowed-tools is a permission filter, not a loader. When they are unavailable, fall back to the read-only Bash commands whitelisted above: rg/grep for search, ls for enumeration, head/file/wc for sniffing, jq for JSON ingest. Bash is otherwise permitted only for python3 .claude/skills/patch/scripts/checkpoint.py (state I/O) and vuln-pipeline patch (execution-verified delegate). find is NOT permitted.
Write scope. The Write tool may target ONLY paths under ./PATCHES/ and ./.patch-state/. Never write into --repo, never git apply, never patch, never edit target source. If a step seems to require it, the step is wrong.
Checkpointing (runs before Phase 0 and after every phase)
State persists to ./.patch-state/ so a fresh /patch session resumes without re-spawning patch or reviewer subagents. All checkpoint I/O goes through python3 .claude/skills/patch/scripts/checkpoint.py (atomic, JSON-validated). The Write→--from pattern keeps repo-derived bytes out of Bash argv; never pass payload via heredoc or stdin.
State files: progress.json (single source of truth: {"status": "running"|"complete", "phase_done": N, "shards_done": [...]}), phaseN.json, _chunk.tmp.
Start of run. Bash: python3 .claude/skills/patch/scripts/checkpoint.py load ./.patch-state
status == "absent"OR"complete", OR--freshin$ARGUMENTS→
fresh start. Bash: python3 .claude/skills/patch/scripts/checkpoint.py reset ./.patch-state, proceed to Phase 0.
status == "running"withphase_done == N→ resume. Read
phase0.json..phaseN.json in order (and any shard_*.json listed in shards_done), merge into working state, print Resuming from checkpoint: Phase N complete, skip to Phase N+1. Do not re-spawn any subagent whose output is already checkpointed.
End of every phase N. Write tool → ./.patch-state/_chunk.tmp with the phase's JSON, then Bash: python3 .claude/skills/patch/scripts/checkpoint.py save ./.patch-state --from ./.patch-state/_chunk.tmp
End of run. After writing PATCHES.md and PATCHES.json, Bash: python3 .claude/skills/patch/scripts/checkpoint.py done ./.patch-state 4
Phase 0: Parse arguments and detect mode
0a. Parse $ARGUMENTS
Extract findings path (first positional), --repo (default .), --top, --id, --model, --fresh. If no findings path, stop and ask.
0b. Detect mode
Inspect the findings path:
- execution-verified mode when the path is a directory containing
reports/manifest.jsonl OR found_bugs.jsonl OR run_*/result.json (pipeline output). The findings have PoC bytes + ASAN traces + reproduction commands; the pipeline's verification ladder applies.
- static mode otherwise:
TRIAGE.json,VULN-FINDINGS.json, generic
finding JSON, or markdown. No PoC; the oracle is a fresh-context reviewer.
Record mode in working state. The two modes share Phase 1 ingest then fork at Phase 2.
Checkpoint: Write tool → ./.patch-state/_chunk.tmp: {"phase": 0, "mode": "exec"|"static", "args": {repo, top, id, model, findings_path}} Then Bash: python3 .claude/skills/patch/scripts/checkpoint.py save ./.patch-state 0 mode --from ./.patch-state/_chunk.tmp
Phase 1: Ingest and normalize
Same input contract as /triage Phase 1. Normalize every input format to a flat findings[] of dicts. Pull what's present; never guess what's absent.
1a. Recognized containers (priority order)
TRIAGE.json— read.findings[]. **Filter to `verdict ==
"true_positive"`.** This is the canonical input: already verified, deduped, ranked, owner-tagged.
VULN-FINDINGS.json— read.findings[]. Unverified; print
Warning: VULN-FINDINGS.json is unverified scanner output. Consider /triage first. and continue.
- Pipeline results directory — one finding per
reports/bug_NN/.
Map report.json → description, crash.crash_type → category, ASAN top-frame → file/line. Record bug_id = NN for the --bug N delegate flag.
- Generic
*.jsonwith a top-level list or afindings/results/
issues/vulnerabilities array.
1b. Field aliases (canonical ← also-accept)
| Canonical | Also accept | |------------------|----------------------------------------------------------| | file | path, location.file, filename | | line | line_number, location.line, lineno | | category | type, cwe, rule_id, crash_type | | severity | severity_rating, level, priority | | title | name, summary, message | | description | details, report, body, evidence, rationale | | recommendation | fix, remediation, mitigation | | owner_hint | owner, component |
Attach id (f001, f002, ... in ingest order; preserve existing ids from TRIAGE.json) and source (relative path of the file it came from).
1c. Filter and order
- If
--id fNNN: keep only that finding. - If
--top N(static mode): sort byseverityHIGH > MEDIUM > LOW then
confidence desc, keep the first N.
- Drop findings with no
file(cannot patch what cannot be located). Record
them as skipped with reason "no source location".
1d. Locate the target codebase (static mode)
Resolve --repo. For the first 5 findings with a file, check the path resolves under repo (try as-given, then with common prefixes stripped). If none resolve, stop: tell the user the cited files aren't reachable and suggest a --repo value.
Checkpoint: Write tool → ./.patch-state/_chunk.tmp: {"phase": 1, "mode": ..., "findings": [...], "skipped": [...], "repo": ...} Then Bash: python3 .claude/skills/patch/scripts/checkpoint.py save ./.patch-state 1 ingest --from ./.patch-state/_chunk.tmp
Phase 2: Generate patches
Forks on mode.
2A. Execution-verified mode — delegate to the pipeline
The pipeline already implements the build → reproduce → regress → re-attack ladder with executable oracles. Do not reimplement it.
For each finding (or once for the whole directory if no --id/--top filter), Bash:
vuln-pipeline patch --model [--bug ]
The pipeline writes /reports/bug_NN/{patch.diff, patch_result.json} itself. After it returns, Read each patch_result.json and copy verdict + rationale into working state. Set verified: "ladder_passed" when verdict.passed == true, else verified: "ladder_failed".
If the CLI exits non-zero (no build_command, missing target config), record the stderr as the finding's error and continue with remaining findings.
Skip Phase 3 (the ladder is the verifier). Proceed to Phase 4.
Checkpoint per finding: Write tool → ./.patch-state/_chunk.tmp = {"id": ..., "verified": ..., "verdict": ..., "diff_path": ...}, then Bash: python3 .claude/skills/patch/scripts/checkpoint.py shard ./.patch-state --from ./.patch-state/_chunk.tmp. After all findings, write the consolidated phase payload to _chunk.tmp then: python3 .claude/skills/patch/scripts/checkpoint.py save ./.patch-state 2 generate --from ./.patch-state/_chunk.tmp
2B. Static mode — one patch subagent per finding
One Task per finding, all in a SINGLE assistant message for parallel execution. subagent_type: "general-purpose". Never set run_in_background — you need the diff text, not an async handle.
Each subagent has read-only access to --repo. It cannot modify the target; it emits the diff as text in its response. The orchestrator writes that text to PATCHES/bug_NN/patch.diff.
Patch subagent prompt (assemble once, reuse per finding)
The full patch-author prompt lives in references/prompts.md § Patch subagent prompt (Phase 2B). Read it at the start of Phase 2B and use it verbatim, substituting the per-finding fields (see #### Spawn below).
Spawn
For each finding in findings[], build a Task call with the prompt above (substituting {REPO_PATH}, {id}, {file}, {line}, {category}, {severity}, {title}, {description}, {recommendation}, and a fresh {nonce} per spawn — see the references/prompts.md preamble; it isolates the attacker-influenced finding text). description: "patch {id}".
If len(findings) > ~40, shard into sequential batches of ~40 (each batch one message). Per-finding shard checkpoint after each result is parsed.
If any Task call returns status: "async_launched" instead of the subagent's text, the runtime backgrounded it. Pick one recovery and use it for the whole batch:
- If completion notifications arrive in your conversation: parse each
subagent's tagged blocks from its notification result as it lands. Do not end your turn until every finding is accounted for.
- If notifications do not arrive: do NOT poll transcript files. Re-spawn
the missing patch subagents in a fresh Task batch (smaller shard, e.g. 10) and use the synchronous results. The same recovery applies to reviewer subagents in Phase 3.
Parse
From each Task result, extract the five tagged blocks. Tolerate leading/ trailing whitespace, stray `` fences, and HTML-escaped entities (< > & — some runtimes escape angle brackets in notification payloads; unescape before writing the diff). If is NONE or empty, mark status: "nopatch". Otherwise write the diff text to ./PATCHES/bugNN/patch.diff (NN = zero-padded index in sorted order) and record rationale, variantschecked, bypassconsidered, test_note`.
Checkpoint per finding: Write tool → ./.patch-state/_chunk.tmp = {"id": ..., "bug_nn": "NN", "status": ..., "rationale": ..., ...}, then Bash: python3 .claude/skills/patch/scripts/checkpoint.py shard ./.patch-state --from ./.patch-state/_chunk.tmp. After all findings, write the consolidated phase payload to _chunk.tmp then: python3 .claude/skills/patch/scripts/checkpoint.py save ./.patch-state 2 generate --from ./.patch-state/_chunk.tmp
Phase 3: Independent review (static mode only)
One reviewer subagent per generated diff, all in ONE message, subagent_type: "general-purpose".
The reviewer never sees the finding's description, recommendation, or the patch author's rationale. It gets only {file, line, category} plus the raw diff bytes, and re-derives whether the diff is a minimal, in-scope fix by reading the source itself. This keeps any instructions embedded in finding prose from reaching both the author and the gate.
Reviewer prompt (assemble once, reuse per diff)
The full reviewer prompt lives in references/prompts.md § Reviewer prompt (Phase 3). Read it at the start of Phase 3 and use it verbatim, substituting {REPO_PATH}, {file}, {line}, {category}, the diff, and a fresh {nonce} per spawn (it wraps the diff as untrusted data). Pass it ONLY those fields — never the finding prose or author rationale.
Spawn and parse
One Task per finding with status != "no_patch". Parse the trailing block. Attach review, style_score, out_of_scope_hunks, review_reason to the finding. Set verified: "static_review_only" for every static-mode result regardless of ACCEPT/REJECT — the label describes the verification class, not the outcome.
Checkpoint: Write tool → ./.patch-state/_chunk.tmp: {"phase": 3, "findings": [...]} Then Bash: python3 .claude/skills/patch/scripts/checkpoint.py save ./.patch-state 3 review --from ./.patch-state/_chunk.tmp
Phase 4: Output
4a. Per-finding patch_result.json
For each finding (both modes), Write ./PATCHES/bug_NN/patch_result.json:
{
"id": "f003",
"source": "TRIAGE.json#2",
"title": "...",
"file": "...",
"line": 0,
"category": "...",
"severity": "HIGH",
"owner_hint": "...",
"mode": "exec" | "static",
"verified": "ladder_passed" | "ladder_failed" | "static_review_only",
"review": "ACCEPT" | "REJECT" | null,
"style_score": 0,
"out_of_scope_hunks": [],
"rationale": "...",
"variants_checked": "...",
"bypass_considered": "...",
"test_note": "...",
"review_reason": "...",
"verdict": { "t0_builds": true, "...": "(exec mode only, from pipeline)" }
}
In exec mode, also Read the pipeline's /reports/bug_NN/patch.diff and Write its bytes to ./PATCHES/bug_NN/patch.diff so both modes land in the same place.
4b. ./PATCHES.json
{
"patch_completed": true,
"mode": "exec" | "static",
"repo": "...",
"summary": {
"input_count": 0,
"patched": 0,
"no_patch": 0,
"accepted": 0,
"rejected": 0,
"ladder_passed": 0
},
"findings": [ { ...patch_result.json shape... } ]
}
4c. ./PATCHES.md (incremental)
Step 1 — header. Write tool → ./PATCHES.md (clobbers prior):
````markdown
Candidate Patches
{if mode == "static":} > Static review only. These diffs were authored and reviewed by > independent agents reading source. They were NOT compiled, run, or > re-attacked. Read each diff yourself before applying — see § "Reviewing > generated patches" in the skill for what to look for.
{if mode == "exec":} > Execution-verified. Each diff passed (or failed) the external harness's > verification ladder: build → reproduce → regress → re-attack. The ladder > proves the crash is gone, not that the diff introduces no new problems.
Input: {findings_path} · Repo: {repo} · {N} findings → {M} diffs
````
Step 2 — per finding (sorted: ACCEPT/ladder_passed first, then by severity). Write ./.patch-state/_chunk.tmp:
````markdown
bug_{NN}: [{severity}] {title} ({id})
{file}:{line} · {category} · owner: {ownerhint or "?"} Status: {verified} · review {review or "n/a"} · style {stylescore or "n/a"}/10 Diff: PATCHES/bug_{NN}/patch.diff ({hunk count} hunks, {line count} lines)
Rationale: {rationale} Variants checked: {variantschecked} Bypass considered: {bypassconsidered} {if review == "REJECT":} > Rejected by reviewer: {reviewreason} {if outofscopehunks:} > Out-of-scope hunks: {outofscope_hunks}
````
Then checkpoint.py append ./PATCHES.md --from ./.patch-state/_chunk.tmp.
Step 3 — footer. Append a ## Skipped table for findings with no file or status == "no_patch", one line each with the reason.
Checkpoint (final): Bash: python3 .claude/skills/patch/scripts/checkpoint.py done ./.patch-state 4
4d. Terminal summary
U
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: air-gapped
- Source: air-gapped/skills
- 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.