Install
$ agentstack add skill-nerdy-krishna-securecoder-securecoder-scan ✓ 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 Used
- ● 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-scan
You are running the /securecoder-scan skill. Your job is to audit the user's codebase for security findings and write the results to .securecoder/runs//.
> Scope. All three scan modes are fully implemented and usable: SAST-only (Semgrep + Bandit + Gitleaks + OSV-scanner), LLM-compliance-only (ASVS v5 — and MASVS / Proactive Controls when configured), and Both. The mode picker in pre-flight step 3 routes between them; the SAST tools run in Phase A and the LLM compliance pass runs in Phase B.
Pre-flight
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.
Every relative path below is rooted here. Capture it as PROJECT_ROOT.
2. Load configuration
Read /.securecoder/config.json if it exists and is parseable. Otherwise use these defaults:
{
"schema_version": "1.0",
"frameworks": ["asvs-v5"],
"severity_floor": "low",
"default_fix_scope": ["critical", "high"],
"git": { "push_strategy": "commit-local-push-at-end" },
"languages": [],
"rule_pins": {},
"tools": {},
"custom_sources": []
}
If the file is missing, mention once: "Running with default configuration. Run /securecoder-setup to customize."
2.5 Resolve the scan-output gitignore strategy
/securecoder-scan writes the full vulnerability picture of the codebase to .securecoder/runs/. That data is sensitive and usually shouldn't land on a shared remote. The git.gitignore_strategy field in config.json controls how the project-root .gitignore reflects that:
runs-and-reviews— root.gitignoreignores.securecoder/runs/+.securecoder/reviews/;config.json/suppressions.jsonstay team-shared and committed. Recommended.whole-folder— root.gitignoreignores the entire.securecoder/directory.none— securecoder leaves the root.gitignorealone.
Resolve the strategy:
- If
config.git.gitignore_strategyis set to one of the three values above, use it. Skip to step 3. - If it's unset (no
config.json, or the field is absent) and the project root is not a git repository, treat the strategy asnoneand skip to step 3 — there's no.gitignoreworth managing. - If it's unset and the project root is a git repository, prompt once:
``` securecoder scan results (.securecoder/runs/) contain the full list of vulnerabilities found in this codebase — sensitive data you usually don't want pushed to a shared remote. How should the project-root .gitignore handle securecoder's output?
[recommended] runs-and-reviews — ignore .securecoder/runs/ and .securecoder/reviews/; keep config.json + suppressions.json team-shared and committed whole-folder ignore the entire .securecoder/ directory (also stops config.json + suppressions.json being shared; files already committed under .securecoder/ stay tracked until you git rm --cached them) none don't touch the root .gitignore ```
Persist the answer so this prompt never repeats: merge {"git": {"gitignore_strategy": ""}} into /.securecoder/config.json — create the file from the step-2 default schema if it doesn't exist, otherwise preserve every existing field and only set git.gitignore_strategy.
Capture the resolved value as GITIGNORE_STRATEGY for step A.12.b.
3. Ask which mode to run
Show three options with token warnings. Use whatever interactive prompt mechanism the host agent supports.
- SAST only — Runs Semgrep, Bandit, Gitleaks, and OSV-scanner. Free in LLM tokens. Typical wall time: under a minute for a small repo, a few minutes for a large one.
- LLM compliance only — Runs the OWASP ASVS v5 compliance review (one LLM call per relevant file × chapter pair). Token-heavy: typical cost is dollars-to-tens-of-dollars depending on repo size and host model. See cost estimate below.
- Both (Recommended for thorough audits) — SAST first, then compliance. SAST findings often reveal issues the compliance pass would also flag, plus stuff compliance misses.
4. Generate the run ID and run directory
RUN_ID="$(date -u +%Y%m%dT%H%M%SZ)"
STARTED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
RUN_DIR="$PROJECT_ROOT/.securecoder/runs/$RUN_ID"
mkdir -p "$RUN_DIR"
cat > "$RUN_DIR/log.md" .enabled` if present, else `true`.
- `path`: optional `config.tools..path` for using a system-installed binary instead of the cached one.
**Auto-skip OSV-scanner if no dependency manifest exists.** Before deciding to run OSV-scanner, check whether any of these manifests live in the project (search up to 3 levels deep):
package.json, package-lock.json, yarn.lock, pnpm-lock.yaml, requirements.txt, pyproject.toml, poetry.lock, Pipfile.lock, go.sum, go.mod, Cargo.lock, Gemfile.lock, composer.lock, pubspec.lock, mix.lock
If none are present, set OSV-scanner's status to `skipped_no_lockfile` and don't attempt to install or run it. Note this in the run log.
### A.1 Detect OS + architecture
```bash
OS="$(uname -s | tr '[:upper:]' '[:lower:]')" # darwin | linux
ARCH_RAW="$(uname -m)"
case "$ARCH_RAW" in
x86_64|amd64) ARCH=amd64 ;;
arm64|aarch64) ARCH=arm64 ;;
*) ARCH="$ARCH_RAW" ;;
esac
Used by binary downloads in A.2.c and A.2.d. For Windows hosts, detect via $env:OS == 'Windows_NT' or equivalent and substitute OS=windows.
A.2 Ensure each enabled tool is installed
The user gave a one-time consent the first time any tool was installed (recorded at ~/.cache/securecoder/manifest.json). Per-version installs after that are silent. If the consent record doesn't exist yet, ask once:
> securecoder needs to install up to four tools (~200MB total): Semgrep, Bandit, Gitleaks, OSV-scanner. They'll be cached under ~/.cache/securecoder/tools/ and never touch your system Python or PATH. Proceed?
Record consent on approval:
mkdir -p "$HOME/.cache/securecoder"
cat > "$HOME/.cache/securecoder/manifest.json" /installed.json` against the pinned version below; install or upgrade only on mismatch.
#### A.2.a Semgrep — pinned `1.91.0`
```bash
SEMGREP_VERSION="1.91.0"
TOOL_DIR="$HOME/.cache/securecoder/tools/semgrep"
INSTALLED="$TOOL_DIR/installed.json"
SEMGREP_BIN="$TOOL_DIR/venv/bin/semgrep"
if [ ! -x "$SEMGREP_BIN" ] || ! grep -q "\"version\": \"$SEMGREP_VERSION\"" "$INSTALLED" 2>/dev/null; then
mkdir -p "$TOOL_DIR"
python3 -m venv "$TOOL_DIR/venv"
"$TOOL_DIR/venv/bin/pip" install --quiet --upgrade pip
"$TOOL_DIR/venv/bin/pip" install --quiet "semgrep==$SEMGREP_VERSION"
cat > "$INSTALLED" /dev/null; then
mkdir -p "$TOOL_DIR"
python3 -m venv "$TOOL_DIR/venv"
"$TOOL_DIR/venv/bin/pip" install --quiet --upgrade pip
"$TOOL_DIR/venv/bin/pip" install --quiet "bandit==$BANDIT_VERSION"
cat > "$INSTALLED" /dev/null; then
mkdir -p "$TOOL_DIR"
case "$ARCH" in
amd64) GLA="x64" ;;
*) GLA="$ARCH" ;;
esac
case "$OS" in
darwin|linux) EXT="tar.gz" ;;
windows) EXT="zip" ;;
esac
ASSET="gitleaks_${GITLEAKS_VERSION}_${OS}_${GLA}.${EXT}"
URL="https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/${ASSET}"
curl -fsSL "$URL" -o "$TOOL_DIR/_pkg.${EXT}"
case "$EXT" in
tar.gz) tar -xzf "$TOOL_DIR/_pkg.tar.gz" -C "$TOOL_DIR" gitleaks ;;
zip) (cd "$TOOL_DIR" && unzip -o _pkg.zip gitleaks.exe && mv gitleaks.exe gitleaks) ;;
esac
rm -f "$TOOL_DIR/_pkg.${EXT}"
chmod +x "$GITLEAKS_BIN" 2>/dev/null || true
cat > "$INSTALLED" /dev/null; then
mkdir -p "$TOOL_DIR"
EXT=""
[ "$OS" = "windows" ] && EXT=".exe"
ASSET="osv-scanner_${OSV_VERSION}_${OS}_${ARCH}${EXT}"
URL="https://github.com/google/osv-scanner/releases/download/v${OSV_VERSION}/${ASSET}"
curl -fsSL "$URL" -o "$OSV_BIN"
chmod +x "$OSV_BIN" 2>/dev/null || true
cat > "$INSTALLED" Source `@` needs network access to install. Either connect to the internet and re-run, or pre-populate `~/.cache/securecoder/tools//` from another machine. To skip this tool, set `config.tools..enabled = false` in `.securecoder/config.json`.
### A.3 Fetch Semgrep rule packs (Semgrep only)
Same as v0.2.0. Pinned upstream: **`returntocorp/semgrep-rules` at branch `main`**, content-addressed by the resulting commit SHA. The other three tools ship rules bundled and need no rule fetch.
```bash
RULES_REPO="https://github.com/returntocorp/semgrep-rules.git"
RULES_BRANCH="main"
RULES_CACHE_ROOT="$HOME/.cache/securecoder/rules/semgrep"
TMP_CLONE="$RULES_CACHE_ROOT/_tmp_clone"
mkdir -p "$RULES_CACHE_ROOT"
REUSE_DIR=""
for d in "$RULES_CACHE_ROOT"/*/; do
[ -d "$d" ] || continue
if [ -f "$d/manifest.json" ]; then
META_BRANCH="$(python3 -c "import json; print(json.load(open('$d/manifest.json')).get('branch',''))" 2>/dev/null || true)"
if [ "$META_BRANCH" = "$RULES_BRANCH" ]; then
REUSE_DIR="$d"
break
fi
fi
done
if [ -z "$REUSE_DIR" ]; then
rm -rf "$TMP_CLONE"
git clone --depth 1 --branch "$RULES_BRANCH" "$RULES_REPO" "$TMP_CLONE" 2>&1
SHA="$(git -C "$TMP_CLONE" rev-parse HEAD)"
FINAL_DIR="$RULES_CACHE_ROOT/$SHA"
if [ -d "$FINAL_DIR" ]; then
rm -rf "$TMP_CLONE"
else
mv "$TMP_CLONE" "$FINAL_DIR"
cat > "$FINAL_DIR/manifest.json" /scripts/repo_walker.py" "$PROJECT_ROOT" \
--output "$RUN_DIR/repo_map.json"
Read the resulting languages map. Use it to:
- Pick Semgrep rule subdirs per the table below. Always also include
$RULES_DIR/generic/and$RULES_DIR/owasp/(when present). - Decide whether Bandit runs — only if
python≥ 1 file. - Decide whether Gitleaks runs — always (file-type agnostic).
| Detected language | Semgrep rules subdir | | --- | --- | | python | python/ | | javascript | javascript/ | | typescript | typescript/ | | go | go/ | | rust | rust/ | | java | java/ | | kotlin | kotlin/ | | ruby | ruby/ | | php | php/ | | csharp | csharp/ | | swift | swift/ | | c, cpp | c/ | | bash | bash/ | | terraform | terraform/ | | dockerfile | dockerfile/ | | html, yaml, json, sql, css, markdown, toml | (skip) |
A.5 Run each enabled tool
Run sequentially. For each tool, time it, capture stderr to a per-tool log, write raw JSON to __raw.json in the run dir.
Per-tool soft failure policy. If a tool fails to run (crashes, exits with no parseable JSON, times out), log it to log.md with status failed, set phases.sast.per_tool..status = "failed" in the manifest, and continue with the other tools. The overall scan still succeeds — just with fewer findings.
A.5.a Semgrep
SEMGREP_RAW="$RUN_DIR/_semgrep_raw.json"
"$SEMGREP_BIN" --metrics=off --quiet --json --output "$SEMGREP_RAW" \
flag per selected subdir from A.4> \
"$PROJECT_ROOT" 2> "$RUN_DIR/_semgrep_stderr.log" || true
Do NOT pass --error — Semgrep finding something is a successful scan, not a failure. The || true lets us check JSON existence rather than exit code.
A.5.b Bandit (skip if no Python files in repo)
BANDIT_RAW="$RUN_DIR/_bandit_raw.json"
"$BANDIT_BIN" -r "$PROJECT_ROOT" -f json -o "$BANDIT_RAW" \
--exit-zero -x "**/.securecoder/**,**/node_modules/**,**/.venv/**,**/venv/**" \
2> "$RUN_DIR/_bandit_stderr.log" || true
--exit-zero makes Bandit always exit 0; we determine success by whether $BANDIT_RAW is valid JSON.
A.5.c Gitleaks
GITLEAKS_RAW="$RUN_DIR/_gitleaks_raw.json"
"$GITLEAKS_BIN" detect --no-banner --report-format json \
--report-path "$GITLEAKS_RAW" --source "$PROJECT_ROOT" \
--exit-code 0 2> "$RUN_DIR/_gitleaks_stderr.log" || true
--exit-code 0 makes Gitleaks exit 0 even when secrets are found (success = scan ran; secrets = findings, not error).
When the repo has no .git/ directory, Gitleaks falls back to filesystem mode, which is what we want for repos outside version control.
A.5.d OSV-scanner (skip if phases.sast.per_tool.osv-scanner.status was set to skipped_no_lockfile in A.0)
OSV_RAW="$RUN_DIR/_osv_raw.json"
"$OSV_BIN" --format json --output "$OSV_RAW" "$PROJECT_ROOT" \
2> "$RUN_DIR/_osv_stderr.log" || true
OSV network handling. OSV-scanner needs api.osv.dev reachable. If the network call fails, OSV exits non-zero and $OSV_RAW may be empty or malformed. Record phases.sast.per_tool.osv-scanner.status = "failed_no_network" and continue. Don't abort the whole scan.
A.6 Normalize each tool's output
For each tool that produced parseable JSON, run its normalizer. The normalizers all accept the same arguments and emit JSONL to the per-tool intermediate file.
python3 "/scripts/normalize_semgrep.py" "$SEMGREP_RAW" --cwe-table "/references/cwe-to-framework.json" --repo-root "$PROJECT_ROOT" --output "$RUN_DIR/_findings_semgrep.jsonl"
python3 "/scripts/normalize_bandit.py" "$BANDIT_RAW" --cwe-table "/references/cwe-to-framework.json" --repo-root "$PROJECT_ROOT" --output "$RUN_DIR/_findings_bandit.jsonl"
python3 "/scripts/normalize_gitleaks.py" "$GITLEAKS_RAW" --cwe-table "/references/cwe-to-framework.json" --repo-root "$PROJECT_ROOT" --output "$RUN_DIR/_findings_gitleaks.jsonl"
python3 "/scripts/normalize_osv.py" "$OSV_RAW" --cwe-table "/references/cwe-to-framework.json" --repo-root "$PROJECT_ROOT" --output "$RUN_DIR/_findings_osv.jsonl"
(Only run a normalizer when its raw input exists and the corresponding tool's status is "ok".)
A.7 Merge per-tool JSONL into one findings.jsonl
: > "$RUN_DIR/findings.jsonl" # truncate
for tool in semgrep bandit gitleaks osv; do
intermediate="$RUN_DIR/_findings_${tool}.jsonl"
[ -s "$intermediate" ] && cat "$intermediate" >> "$RUN_DIR/findings.jsonl"
done
Canonical IDs are deterministic and per-tool prefixes in source keep cross-tool collisions impossible.
A.7.3 Scan for in-source suppression annotations (v1.2.0)
Before applying suppressions, walk the project for # securecoder: ignore (and // securecoder: ignore) annotations. The scanner emits ephemeral suppression entries the next step merges with the persistent suppressions.json:
python3 "/scripts/scan_annotations.py" "$PROJECT_ROOT" \
--output "$RUN_DIR/_annotations.json"
Annotation syntax:
# securecoder: ignore— applies to the next non-blank code line# securecoder: ignore reason="..."— same, with explicit reason# securecoder: ignore reason="..." expires="2027-01-01"— same, with expiry- Inline form (end of code line) — applies to that line
The scanner outputs a JSON array of ephemeral entries with source: "annotation" and created_by: "". Block comments (/* ... */) are not yet recognized — v1.2.0 line comments only.
A.7.5 Apply suppressions
Mark matching findings as suppressed in-place. Merges persistent entries from .securecoder/suppressions.json (if present) with ephemeral annotation entries from A.7.3 above. See [docs/design.md § 3.9](../../../docs/design.md) for the matching semantics and most-specific-wins resolution.
SUPPRESSIONS_PATH="$PROJECT_ROOT/.securecoder/suppressions.json"
ANN_ARG=""
if [ -s "$RUN_DIR/_annotations.json" ]; then
ANN_ARG="--annotations $RUN_DIR/_annotations.json"
fi
if [ -f "$SUPPRESSIONS_PATH" ] || [ -n "$ANN_ARG" ]; then
python3 "/scripts/apply_suppressions.py" \
"$RUN_DIR/findings.jsonl" \
--suppressions "${SUPPRESSIONS_PATH:-/dev/null}" \
$ANN_ARG \
--output "$RUN_DIR/findings.jsonl" \
--stats "$RUN_DIR/_suppression_stats.json"
else
# No suppressions file AND no annotations = no-op; create an empty
# stats record so the manifest writer below has a consistent input.
echo '{"totals":{"findings":0,"findings_active":0,"findings_suppressed":0},"suppressed_by_entry":{}}' \
> "$RUN_DIR/_suppression_stats.json"
fi
Findings matching a suppression get `status: "suppressed"
…
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.