Most developers assume security scanning requires expensive SaaS subscriptions or enterprise CI/CD platforms. It doesn't.
Enterprise-grade SAST (Static Application Security Testing) and dependency scanning tools are available as free, public Docker images. They work on any codebase regardless of where it's hosted — GitHub, Bitbucket, GitLab, or a local directory that isn't even a git repo.
This guide covers:
- Running security scans with Docker
- Feeding results into Claude Code for automated analysis
- Setting up Claude Code hooks to scan on every commit
- Adding scans to CI/CD pipelines
- Language-specific tips for PHP, Node.js, Python, Go, Java, Ruby, and C#
Prerequisites
- Docker installed and running
- Claude Code installed (for automated analysis)
- A codebase to scan (any language, any git host or none)
That's it. No accounts, tokens, or licenses required for the scanners.
Step 1: Run the Security Scan
Two scanners are available as public Docker images. No login or token needed to pull them.
SAST (Static Application Security Testing)
Scans your source code for vulnerabilities — SQL injection, XSS, insecure deserialization, hardcoded secrets, and more. Based on Semgrep with thousands of maintained rules.
docker run --rm --pull always \
-v "$(pwd):/code" \
registry.gitlab.com/security-products/semgrep \
/analyzer run --target-dir /code --artifact-dir /code
Output: gl-sast-report.json in your current directory.
Dependency Scanning
Scans your lockfiles (composer.lock, package-lock.json, requirements.txt, go.sum, etc.) for known CVEs in third-party dependencies.
docker run --rm --pull always \
-v "$(pwd):/code" \
registry.gitlab.com/security-products/gemnasium \
/analyzer run --target-dir /code --artifact-dir /code
Output: gl-dependency-scanning-report.json in your current directory.
Important: Mount Your Actual Source Code
The -v "$(pwd):/code" flag mounts your local directory into the Docker container as /code. This must point to where your source code actually lives — not the repo root if your code is in a subdirectory.
For example, if your repo looks like this:
my-project/
deploy/
infra/
docs/
app/ <- actual source code is here
src/
public_html/
config/
Then mount app/, not the repo root:
# Wrong — scans deploy scripts, infra templates, docs
docker run --rm --pull always -v "$(pwd):/code" ...
# Right — scans your actual application code
docker run --rm --pull always -v "$(pwd)/app:/code" ...
The report JSON file will be written into whichever directory you mount, so check there for the output.
Directory Depth Limit
Both scanners default to a maximum depth of 2 directories. If your source code or lockfiles are nested more than 2 levels deep from the mounted directory, the scanner won't find them.
Increase the depth with the SEARCH_MAX_DEPTH environment variable (works for both scanners), or use DS_MAX_DEPTH for the dependency scanner specifically:
# Scan up to 5 levels deep
docker run --rm --pull always \
-e SEARCH_MAX_DEPTH=5 \
-v "$(pwd)/app:/code" \
registry.gitlab.com/security-products/semgrep \
/analyzer run --target-dir /code --artifact-dir /code
# Dependency scanner — unlimited depth
docker run --rm --pull always \
-e DS_MAX_DEPTH=-1 \
-v "$(pwd)/app:/code" \
registry.gitlab.com/security-products/gemnasium \
/analyzer run --target-dir /code --artifact-dir /code
Setting DS_MAX_DEPTH=-1 disables the limit entirely for the dependency scanner. For the SAST scanner, pick a number that covers your deepest source file.
Keeping Scanners Updated
The Docker images are tagged releases, updated roughly weekly to fortnightly with new vulnerability rules and CVE databases. The --pull always flag ensures Docker checks for newer images on each run — it only downloads changed layers, so subsequent pulls are fast.
Step 2: Analyse Results with Claude Code
The scanners output structured JSON. Feed this directly to Claude Code for analysis:
# Run the scan
docker run --rm --pull always \
-v "$(pwd):/code" \
registry.gitlab.com/security-products/semgrep \
/analyzer run --target-dir /code --artifact-dir /code
Then in Claude Code:
Read gl-sast-report.json and:
1. List all Critical and High findings with plain-English explanations
2. Identify any likely false positives and explain why
3. Suggest specific code fixes for each real vulnerability
4. Prioritise by exploitability — what should I fix first?
Claude Code will:
- Explain each vulnerability in plain English — what it is and why it matters
- Prioritise by severity — Critical and High findings first, based on real exploitability
- Suggest specific code fixes — not generic advice, but changes to your actual code
- Filter false positives — identify findings that aren't exploitable in your context
- Fix the code — apply the changes directly if you ask it to
This replaces hours of manual triage with a focused, prioritised action list.
Understanding the Report Format
Both scanners output JSON following a standard schema. Key fields:
- severity: Critical, High, Medium, Low, Info
- location.file + location.start_line: Exact file and line number
- identifiers: Links to CWE, CVE, or other vulnerability databases
- solution: Generic remediation advice (Claude Code provides better, codebase-specific suggestions)
Step 3: Automate with Claude Code Hooks
Claude Code supports hooks — shell commands that run automatically at specific points in your workflow. Configure a hook that runs the security scanner on every commit and feeds the results directly into Claude Code for analysis.
Setup: Scan on Every Commit
Step 1: Create the hook script at .claude/hooks/security-scan.sh:
#!/bin/bash
INPUT=$(cat)
# Run SAST scanner
docker run --rm --pull always \
-v "$(pwd):/code" \
registry.gitlab.com/security-products/semgrep \
/analyzer run --target-dir /code --artifact-dir /code 2>/dev/null
# Parse results and feed back to Claude Code
if [ -f gl-sast-report.json ]; then
VULN_COUNT=$(jq '.vulnerabilities | length' gl-sast-report.json)
CRITICAL=$(jq '[.vulnerabilities[] | select(.severity == "Critical" or .severity == "High")] | length' gl-sast-report.json)
if [ "$VULN_COUNT" -gt 0 ]; then
SUMMARY=$(jq -r '.vulnerabilities[] | "[\(.severity)] \(.name) in \(.location.file):\(.location.start_line // "?")"' gl-sast-report.json)
jq -n --arg count "$VULN_COUNT" --arg critical "$CRITICAL" --arg summary "$SUMMARY" '{
hookSpecificOutput: {
hookEventName: "PostToolUse",
additionalContext: "Security scan found \($count) vulnerabilities (\($critical) Critical/High):\n\($summary)"
}
}'
fi
rm -f gl-sast-report.json
fi
exit 0
Step 2: Make it executable: chmod +x .claude/hooks/security-scan.sh
Step 3: Add the hook to .claude/settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/security-scan.sh",
"timeout": 120,
"if": "Bash(git commit*)"
}
]
}
]
}
}
The "if": "Bash(git commit*)" filter ensures the scan only runs when Claude Code makes a git commit. Now every commit triggers automated scanning and triage.
Alternative: Block Commits with Critical Vulnerabilities
Use a PreToolUse hook instead, and return exit code 2 from the script to prevent the commit when Critical vulnerabilities are found.
Global Setup (All Projects)
Add the hook to ~/.claude/settings.json instead of the project-level file, and every project gets automatic security scanning.
Git Hooks (Without Claude Code)
For scanning in your normal git workflow, use a pre-push hook. Scanning takes 30–60 seconds, so pre-push is the sweet spot — it runs before code leaves your machine without slowing down every commit.
Create .git/hooks/pre-push with the scan commands, check for Critical/High findings, and exit with code 1 to block the push. Use git push --no-verify to override when needed.
CI/CD Integration
The same Docker images work in any CI/CD pipeline:
GitHub Actions
name: Security Scan
on: [push, pull_request]
jobs:
sast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run SAST
run: |
docker run --rm \
-v "${{ github.workspace }}:/code" \
registry.gitlab.com/security-products/semgrep \
/analyzer run --target-dir /code --artifact-dir /code
- name: Upload SAST Report
uses: actions/upload-artifact@v4
if: always()
with:
name: sast-report
path: gl-sast-report.json
GitLab has native support via CI components. Bitbucket Pipelines works with the same Docker commands in a step with the docker service enabled.
Infrastructure as Code Scanning
If your project includes CloudFormation, Terraform, Kubernetes manifests, or Dockerfiles, the Semgrep scanner already includes IaC rules. The same SAST command flags:
- Open security groups (0.0.0.0/0 ingress)
- Unencrypted storage (S3 buckets, RDS instances, EBS volumes)
- Overly permissive IAM policies
- Missing logging/monitoring
- Hardcoded secrets in templates
Language-Specific Tips
The scanners auto-detect your stack by looking for lockfiles and source file extensions.
| Language | Lockfile Needed | Key Vulnerabilities Detected |
|---|---|---|
| PHP | composer.lock | SQL injection, XSS, command injection, insecure deserialization, path traversal |
| Node.js | package-lock.json / yarn.lock | Prototype pollution, XSS, eval injection, ReDoS, JWT misconfiguration |
| Python | requirements.txt / Pipfile.lock | SQL injection, command injection, SSRF, unsafe deserialization, template injection |
| Go | go.sum | SQL injection, command injection, path traversal, race conditions, insecure TLS |
| Java | pom.xml / build.gradle | SQL injection, XXE, insecure deserialization, LDAP injection, SSRF |
| Ruby | Gemfile.lock | SQL injection, XSS, command injection, mass assignment, YAML deserialization |
| C# | .csproj / packages.lock.json | SQL injection, XSS, path traversal, insecure deserialization, XXE |
Quick Reference
# SAST — scans source code
docker run --rm --pull always \
-v "$(pwd):/code" \
registry.gitlab.com/security-products/semgrep \
/analyzer run --target-dir /code --artifact-dir /code
# Dependency Scanning — scans lockfiles
docker run --rm --pull always \
-v "$(pwd):/code" \
registry.gitlab.com/security-products/gemnasium \
/analyzer run --target-dir /code --artifact-dir /code
See the companion case study for a complete walkthrough of applying this guide to a real PHP + React codebase, including all the gotchas and how we resolved them.