SecCodePRM: Step-Level Security Scoring for Code
This skill enables Claude to apply process reward model reasoning to code security — scoring code step-by-step rather than as a monolithic block. Inspired by the SecCodePRM paper, the technique segments code into logical steps (roughly paragraph-level blocks), assigns each step a security risk score considering all prior context, then aggregates scores with risk-sensitive weighting that amplifies the most dangerous regions. This produces dense, localized security feedback that works on complete programs, partial prefixes during generation, and as a ranking signal when choosing between alternative implementations.
When to Use
- When the user asks to review code for security vulnerabilities and wants to know which specific blocks are dangerous, not just a binary safe/unsafe verdict
- When generating security-sensitive code (crypto, auth, memory management, input parsing, SQL queries) and you need to evaluate multiple candidate approaches
- When analyzing incomplete or streaming code to flag emerging risks before the function is finished
- When the user asks to fix a vulnerability and you need to precisely localize the root cause across inter-procedural call chains
- When comparing two or more implementations of the same functionality and choosing the most secure one
- When working with long codebases (1000+ lines) where whole-program reasoning degrades — step-level scoring stays precise
Key Technique
Step-level security scoring breaks code into discrete logical steps — blocks separated by structural boundaries (blank lines, statement groups, function boundaries). Each step receives a security score that reflects its risk in context of all preceding steps. This is fundamentally different from line-level linting (no context) or whole-program classification (no localization). A malloc call is neutral in isolation but dangerous if a prior step set a user-controlled size without bounds checking. Step-level scoring captures this.
Risk-sensitive aggregation converts per-step scores into a program-level verdict by weighting steps inversely to their safety: w_i = exp(-r_i / tau) / sum(exp(-r_j / tau)). Steps with low safety scores get exponentially higher weight. This means a single dangerous step dominates the aggregate even if surrounded by safe code — matching real-world security where one exploitable flaw compromises the entire program regardless of how clean the rest is.
Inference-time scaling for secure generation uses best-of-N sampling: generate K candidate continuations at each step, score each with the process reward model, and select the candidate with highest advantage A_t = r_t - mean(r_t). This steers generation toward secure trajectories without sacrificing functional correctness — the paper shows pass@1 rates actually improve slightly because security-aware selection also filters out structurally broken code.
Step-by-Step Workflow
Segment the code into logical steps. Split at double-newline boundaries (\n\n). Merge fragments that are structurally incomplete (a lone return 0;, a variable declaration without its use). Each step should be a self-contained logical unit: a variable declaration block, a conditional branch, a loop body, a function call sequence.
Build a context chain. For each step t, the evaluation context is the concatenation of all steps 1..t. Never score a step in isolation — a free(ptr) is safe on first occurrence but a use-after-free on second.
Score each step for security risk. Evaluate the margin between safe and unsafe: r_t = P(safe | steps_1..t) - P(unsafe | steps_1..t). Scores range from -1 (certainly vulnerable) to +1 (certainly safe). Flag any step with r_t < 0 as a risk point.
Check for inter-procedural vulnerability propagation. If step t calls a function defined elsewhere, trace the callee's steps. A tainted return value in a callee propagates vulnerability to the caller's step. Annotate the call chain.
Aggregate with risk-sensitive weighting. Compute the program-level score R = sum(w_i * r_i) where weights emphasize the lowest-scoring steps. Use temperature tau between 0.1 (very risk-sensitive, dominated by worst step) and 1.0 (balanced). Default to tau = 0.3 for security-critical reviews.
For vulnerability detection: Report each step with r_t < 0 as a finding, ordered by severity (most negative first). Map each finding to a CWE category: buffer overflow (CWE-125), integer overflow (CWE-190), use-after-free (CWE-416), null pointer dereference (CWE-476), injection (CWE-89), etc.
For secure code generation: When writing new code, mentally evaluate 2-3 alternative implementations at each decision point. Choose the continuation where the cumulative reward is highest. Prefer bounds-checked alternatives, safe library functions, and validated inputs.
For partial-code assessment: When reviewing incomplete code, score all steps written so far. If the trajectory is trending negative (scores decreasing), warn the user before they continue building on a risky foundation.
Verify functional correctness is preserved. After any security-motivated change, confirm the code still fulfills its functional requirements. Security fixes that break functionality are not acceptable — the goal is improved security at equivalent or better utility.
Produce a structured security report. For each flagged step: quote the exact code, state the risk, identify the CWE, suggest a concrete fix, and show the fixed version with its improved step score.
Concrete Examples
Example 1: Localize a buffer overflow in C
User: "Review this C function for security issues"
void process_input(char *user_data, int len) {
char buffer[256];
int adjusted_len = len + 10;
memcpy(buffer, user_data, adjusted_len);
buffer[adjusted_len] = '\0';
printf("Processed: %s\n", buffer);
}
Approach:
- Segment into steps: (a) declaration of
buffer, (b) adjusted_len computation, (c) memcpy call, (d) null termination, (e) printf
- Score each step in context:
- Step (a):
r=+0.8 — fixed-size stack buffer, neutral alone
- Step (b):
r=+0.2 — arithmetic on user-controlled len without overflow check (CWE-190 risk emerging)
- Step (c):
r=-0.7 — copies adjusted_len bytes into 256-byte buffer with no bounds check (CWE-125 triggered)
- Step (d):
r=-0.5 — writes at adjusted_len offset, compounding the overflow
- Step (e):
r=+0.6 — printf itself is safe given prior null termination
- Risk-sensitive aggregate:
R = -0.41 (dominated by steps c and d)
Output:
SECURITY FINDINGS (2 issues, aggregate risk: HIGH)
[CRITICAL] Step 3 (line 7): Heap/stack buffer overflow — CWE-125
memcpy(buffer, user_data, adjusted_len);
`adjusted_len` can exceed 256. No bounds check on `len`.
Fix: if (adjusted_len > sizeof(buffer) - 1) return; // before memcpy
[HIGH] Step 2 (line 5): Integer overflow — CWE-190
int adjusted_len = len + 10;
If len is near INT_MAX, `len + 10` wraps negative, causing
memcpy to receive a huge size_t value.
Fix: if (len < 0 || len > 246) return; // bounds check before arithmetic
Example 2: Secure code generation for SQL query
User: "Write a Python function to look up a user by username from a database"
Approach:
- At the query construction step, evaluate candidates:
- Candidate A:
f"SELECT * FROM users WHERE name = '{username}'" — step score r=-0.9 (CWE-89 SQL injection)
- Candidate B:
cursor.execute("SELECT * FROM users WHERE name = ?", (username,)) — step score r=+0.9 (parameterized)
- Select candidate B (highest reward)
- Continue generation with input validation step
Output:
import sqlite3
def lookup_user(db_path: str, username: str) -> dict | None:
"""Look up a user by username using parameterized query."""
if not username or len(username) > 255:
return None
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
cursor.execute(
"SELECT id, username, email FROM users WHERE username = ?",
(username,),
)
row = cursor.fetchone()
conn.close()
if row is None:
return None
return {"id": row[0], "username": row[1], "email": row[2]}
Step scores: input validation r=+0.8, connection r=+0.7, parameterized query r=+0.9, result handling r=+0.7. Aggregate: R = +0.78.
Example 3: Partial-code prefix assessment
User: "I'm writing a file upload handler, is this start safe so far?"
from flask import request
import os
def upload_file():
f = request.files['file']
filename = f.filename
save_path = os.path.join('/uploads', filename)
Approach:
- Score steps written so far:
- Step 1 (imports):
r=+0.7 — neutral
- Step 2 (get file):
r=+0.5 — no check for missing key, minor
- Step 3 (filename):
r=-0.3 — using raw user-supplied filename (CWE-22 path traversal risk emerging)
- Step 4 (path join):
r=-0.8 — os.path.join('/uploads', '../../../etc/passwd') resolves outside uploads dir
- Trajectory is trending negative — warn before user continues
Output:
WARNING: Security trajectory is negative at step 4.
Step 3-4 introduce path traversal risk (CWE-22):
filename = f.filename # unsanitized user input
save_path = os.path.join('/uploads', filename) # attacker controls path
Before continuing, add:
filename = secure_filename(f.filename) # from werkzeug.utils
save_path = os.path.join('/uploads', filename)
if not os.path.abspath(save_path).startswith('/uploads'):
abort(400)
This raises the step score from r=-0.8 to r=+0.8.
Best Practices
- Do: Score every step in the context of all preceding steps. A
free() call is safe or dangerous depending on what came before it.
- Do: Use risk-sensitive aggregation (low tau) for security reviews. A single critical flaw at step 15 of 50 should dominate the score, not be averaged away.
- Do: Map findings to specific CWE identifiers. "This is insecure" is less useful than "CWE-416: use-after-free due to double free on line 34."
- Do: When generating code, prefer the secure alternative at each decision point even when it requires a few extra lines. The paper shows this preserves or improves functional correctness.
- Avoid: Scoring steps in isolation without context.
ptr = malloc(n) is meaningless without knowing if n was validated.
- Avoid: Treating all steps equally in aggregation. Uniform averaging hides critical vulnerabilities behind a majority of safe code. Always weight toward risk.
Error Handling
- False positives on defensive code: Code that handles error paths (null checks, bounds validation) may look similar to vulnerable patterns. Score the complete conditional block as one step rather than splitting guard and body.
- Cross-file vulnerabilities: When a vulnerability spans multiple files (e.g., tainted data flows from a controller to a model), explicitly reconstruct the inter-procedural call chain before scoring. Scoring one file in isolation will miss the issue.
- Very long functions: If a function exceeds 50 steps, group related steps into macro-blocks (initialization, processing, cleanup) and score at both granularities. Step-level scores within each block, plus block-level aggregation for the function verdict.
- Language-specific patterns: Certain patterns are idiomatic and safe in one language but dangerous in another (e.g., pointer arithmetic in Rust vs C). Calibrate severity to the language's safety guarantees.
Limitations
- This technique is strongest for memory safety (C/C++), injection (SQL/XSS/command), and input validation vulnerabilities. It is less effective for logic bugs, cryptographic misuse (wrong mode of operation), or race conditions that depend on runtime timing rather than code structure.
- Step-level scoring approximates but does not replace full taint analysis or symbolic execution. It cannot prove the absence of vulnerabilities — it identifies likely risk regions.
- The approach works best on imperative, sequential code. Heavily callback-based, event-driven, or reactive code may not segment cleanly into linear steps.
- For production vulnerability scanning, complement this technique with actual static analysis tools (CodeQL, Semgrep) and dynamic testing. This skill provides fast, contextual feedback during authoring, not a formal security audit.
Reference
SecCodePRM: A Process Reward Model for Code Security — Yu et al., 2026. Focus on Section 3 (step segmentation and labeling), Section 4 (risk-sensitive aggregation formula), and Section 5.3 (inference-time scaling for secure generation). The key insight: scoring code security at the step level with risk-weighted aggregation outperforms both static analyzers and whole-program classifiers while preserving functional correctness.
1---2name: seccodeprm-process-reward-code3description: Step-level security scoring for code generation and vulnerability detection using process reward model techniques. Use when asked to: 'review this code for vulnerabilities', 'generate secure code for [task]', 'score security of each code block', 'find the risky parts of this function', 'which lines introduce the vulnerability', 'help me write secure C/C++/Python code'.4---56# SecCodePRM: Step-Level Security Scoring for Code78This skill enables Claude to apply **process reward model reasoning** to code security — scoring code step-by-step rather than as a monolithic block. Inspired by the SecCodePRM paper, the technique segments code into logical steps (roughly paragraph-level blocks), assigns each step a security risk score considering all prior context, then aggregates scores with risk-sensitive weighting that amplifies the most dangerous regions. This produces dense, localized security feedback that works on complete programs, partial prefixes during generation, and as a ranking signal when choosing between alternative implementations.910## When to Use1112- When the user asks to **review code for security vulnerabilities** and wants to know *which specific blocks* are dangerous, not just a binary safe/unsafe verdict13- When **generating security-sensitive code** (crypto, auth, memory management, input parsing, SQL queries) and you need to evaluate multiple candidate approaches14- When analyzing **incomplete or streaming code** to flag emerging risks before the function is finished15- When the user asks to **fix a vulnerability** and you need to precisely localize the root cause across inter-procedural call chains16- When comparing **two or more implementations** of the same functionality and choosing the most secure one17- When working with **long codebases** (1000+ lines) where whole-program reasoning degrades — step-level scoring stays precise1819## Key Technique2021**Step-level security scoring** breaks code into discrete logical steps — blocks separated by structural boundaries (blank lines, statement groups, function boundaries). Each step receives a security score that reflects its risk *in context of all preceding steps*. This is fundamentally different from line-level linting (no context) or whole-program classification (no localization). A `malloc` call is neutral in isolation but dangerous if a prior step set a user-controlled size without bounds checking. Step-level scoring captures this.2223**Risk-sensitive aggregation** converts per-step scores into a program-level verdict by weighting steps inversely to their safety: `w_i = exp(-r_i / tau) / sum(exp(-r_j / tau))`. Steps with low safety scores get exponentially higher weight. This means a single dangerous step dominates the aggregate even if surrounded by safe code — matching real-world security where one exploitable flaw compromises the entire program regardless of how clean the rest is.2425**Inference-time scaling for secure generation** uses best-of-N sampling: generate K candidate continuations at each step, score each with the process reward model, and select the candidate with highest advantage `A_t = r_t - mean(r_t)`. This steers generation toward secure trajectories without sacrificing functional correctness — the paper shows pass@1 rates actually improve slightly because security-aware selection also filters out structurally broken code.2627## Step-by-Step Workflow28291. **Segment the code into logical steps.** Split at double-newline boundaries (`\n\n`). Merge fragments that are structurally incomplete (a lone `return 0;`, a variable declaration without its use). Each step should be a self-contained logical unit: a variable declaration block, a conditional branch, a loop body, a function call sequence.30312. **Build a context chain.** For each step `t`, the evaluation context is the concatenation of all steps `1..t`. Never score a step in isolation — a `free(ptr)` is safe on first occurrence but a use-after-free on second.32333. **Score each step for security risk.** Evaluate the margin between safe and unsafe: `r_t = P(safe | steps_1..t) - P(unsafe | steps_1..t)`. Scores range from -1 (certainly vulnerable) to +1 (certainly safe). Flag any step with `r_t < 0` as a risk point.34354. **Check for inter-procedural vulnerability propagation.** If step `t` calls a function defined elsewhere, trace the callee's steps. A tainted return value in a callee propagates vulnerability to the caller's step. Annotate the call chain.36375. **Aggregate with risk-sensitive weighting.** Compute the program-level score `R = sum(w_i * r_i)` where weights emphasize the lowest-scoring steps. Use temperature `tau` between 0.1 (very risk-sensitive, dominated by worst step) and 1.0 (balanced). Default to `tau = 0.3` for security-critical reviews.38396. **For vulnerability detection:** Report each step with `r_t < 0` as a finding, ordered by severity (most negative first). Map each finding to a CWE category: buffer overflow (CWE-125), integer overflow (CWE-190), use-after-free (CWE-416), null pointer dereference (CWE-476), injection (CWE-89), etc.40417. **For secure code generation:** When writing new code, mentally evaluate 2-3 alternative implementations at each decision point. Choose the continuation where the cumulative reward is highest. Prefer bounds-checked alternatives, safe library functions, and validated inputs.42438. **For partial-code assessment:** When reviewing incomplete code, score all steps written so far. If the trajectory is trending negative (scores decreasing), warn the user before they continue building on a risky foundation.44459. **Verify functional correctness is preserved.** After any security-motivated change, confirm the code still fulfills its functional requirements. Security fixes that break functionality are not acceptable — the goal is improved security at equivalent or better utility.464710. **Produce a structured security report.** For each flagged step: quote the exact code, state the risk, identify the CWE, suggest a concrete fix, and show the fixed version with its improved step score.4849## Concrete Examples5051**Example 1: Localize a buffer overflow in C**5253User: "Review this C function for security issues"5455```c56void process_input(char *user_data, int len) {5758 char buffer[256];5960 int adjusted_len = len + 10;6162 memcpy(buffer, user_data, adjusted_len);6364 buffer[adjusted_len] = '\0';6566 printf("Processed: %s\n", buffer);67}68```6970Approach:711. Segment into steps: (a) declaration of `buffer`, (b) `adjusted_len` computation, (c) `memcpy` call, (d) null termination, (e) printf722. Score each step in context:73 - Step (a): `r=+0.8` — fixed-size stack buffer, neutral alone74 - Step (b): `r=+0.2` — arithmetic on user-controlled `len` without overflow check (CWE-190 risk emerging)75 - Step (c): `r=-0.7` — copies `adjusted_len` bytes into 256-byte buffer with no bounds check (CWE-125 triggered)76 - Step (d): `r=-0.5` — writes at `adjusted_len` offset, compounding the overflow77 - Step (e): `r=+0.6` — printf itself is safe given prior null termination783. Risk-sensitive aggregate: `R = -0.41` (dominated by steps c and d)7980Output:81```82SECURITY FINDINGS (2 issues, aggregate risk: HIGH)8384[CRITICAL] Step 3 (line 7): Heap/stack buffer overflow — CWE-12585 memcpy(buffer, user_data, adjusted_len);86 `adjusted_len` can exceed 256. No bounds check on `len`.8788 Fix: if (adjusted_len > sizeof(buffer) - 1) return; // before memcpy8990[HIGH] Step 2 (line 5): Integer overflow — CWE-19091 int adjusted_len = len + 10;92 If len is near INT_MAX, `len + 10` wraps negative, causing93 memcpy to receive a huge size_t value.9495 Fix: if (len < 0 || len > 246) return; // bounds check before arithmetic96```9798**Example 2: Secure code generation for SQL query**99100User: "Write a Python function to look up a user by username from a database"101102Approach:1031. At the query construction step, evaluate candidates:104 - Candidate A: `f"SELECT * FROM users WHERE name = '{username}'"` — step score `r=-0.9` (CWE-89 SQL injection)105 - Candidate B: `cursor.execute("SELECT * FROM users WHERE name = ?", (username,))` — step score `r=+0.9` (parameterized)1062. Select candidate B (highest reward)1073. Continue generation with input validation step108109Output:110```python111import sqlite3112113def lookup_user(db_path: str, username: str) -> dict | None:114 """Look up a user by username using parameterized query."""115 if not username or len(username) > 255:116 return None117118 conn = sqlite3.connect(db_path)119 cursor = conn.cursor()120121 cursor.execute(122 "SELECT id, username, email FROM users WHERE username = ?",123 (username,),124 )125126 row = cursor.fetchone()127 conn.close()128129 if row is None:130 return None131 return {"id": row[0], "username": row[1], "email": row[2]}132```133134Step scores: input validation `r=+0.8`, connection `r=+0.7`, parameterized query `r=+0.9`, result handling `r=+0.7`. Aggregate: `R = +0.78`.135136**Example 3: Partial-code prefix assessment**137138User: "I'm writing a file upload handler, is this start safe so far?"139140```python141from flask import request142import os143144def upload_file():145 f = request.files['file']146 filename = f.filename147148 save_path = os.path.join('/uploads', filename)149```150151Approach:1521. Score steps written so far:153 - Step 1 (imports): `r=+0.7` — neutral154 - Step 2 (get file): `r=+0.5` — no check for missing key, minor155 - Step 3 (filename): `r=-0.3` — using raw user-supplied filename (CWE-22 path traversal risk emerging)156 - Step 4 (path join): `r=-0.8` — `os.path.join('/uploads', '../../../etc/passwd')` resolves outside uploads dir1572. Trajectory is trending negative — warn before user continues158159Output:160```161WARNING: Security trajectory is negative at step 4.162163Step 3-4 introduce path traversal risk (CWE-22):164 filename = f.filename # unsanitized user input165 save_path = os.path.join('/uploads', filename) # attacker controls path166167Before continuing, add:168 filename = secure_filename(f.filename) # from werkzeug.utils169 save_path = os.path.join('/uploads', filename)170 if not os.path.abspath(save_path).startswith('/uploads'):171 abort(400)172173This raises the step score from r=-0.8 to r=+0.8.174```175176## Best Practices177178- **Do:** Score every step in the context of all preceding steps. A `free()` call is safe or dangerous depending on what came before it.179- **Do:** Use risk-sensitive aggregation (low tau) for security reviews. A single critical flaw at step 15 of 50 should dominate the score, not be averaged away.180- **Do:** Map findings to specific CWE identifiers. "This is insecure" is less useful than "CWE-416: use-after-free due to double free on line 34."181- **Do:** When generating code, prefer the secure alternative at each decision point even when it requires a few extra lines. The paper shows this preserves or improves functional correctness.182- **Avoid:** Scoring steps in isolation without context. `ptr = malloc(n)` is meaningless without knowing if `n` was validated.183- **Avoid:** Treating all steps equally in aggregation. Uniform averaging hides critical vulnerabilities behind a majority of safe code. Always weight toward risk.184185## Error Handling186187- **False positives on defensive code:** Code that handles error paths (null checks, bounds validation) may look similar to vulnerable patterns. Score the *complete* conditional block as one step rather than splitting guard and body.188- **Cross-file vulnerabilities:** When a vulnerability spans multiple files (e.g., tainted data flows from a controller to a model), explicitly reconstruct the inter-procedural call chain before scoring. Scoring one file in isolation will miss the issue.189- **Very long functions:** If a function exceeds 50 steps, group related steps into macro-blocks (initialization, processing, cleanup) and score at both granularities. Step-level scores within each block, plus block-level aggregation for the function verdict.190- **Language-specific patterns:** Certain patterns are idiomatic and safe in one language but dangerous in another (e.g., pointer arithmetic in Rust vs C). Calibrate severity to the language's safety guarantees.191192## Limitations193194- This technique is strongest for **memory safety (C/C++)**, **injection (SQL/XSS/command)**, and **input validation** vulnerabilities. It is less effective for **logic bugs**, **cryptographic misuse** (wrong mode of operation), or **race conditions** that depend on runtime timing rather than code structure.195- Step-level scoring approximates but does not replace full **taint analysis** or **symbolic execution**. It cannot prove the absence of vulnerabilities — it identifies likely risk regions.196- The approach works best on **imperative, sequential code**. Heavily callback-based, event-driven, or reactive code may not segment cleanly into linear steps.197- For **production vulnerability scanning**, complement this technique with actual static analysis tools (CodeQL, Semgrep) and dynamic testing. This skill provides fast, contextual feedback during authoring, not a formal security audit.198199## Reference200201[SecCodePRM: A Process Reward Model for Code Security](https://arxiv.org/abs/2602.10418v1) — Yu et al., 2026. Focus on Section 3 (step segmentation and labeling), Section 4 (risk-sensitive aggregation formula), and Section 5.3 (inference-time scaling for secure generation). The key insight: scoring code security at the step level with risk-weighted aggregation outperforms both static analyzers and whole-program classifiers while preserving functional correctness.