AI coding agents are fast, capable, and — without explicit guidance — casually insecure. Claude Code will generate SQL queries without parameterization if you do not tell it otherwise. It will suggest storing tokens in localStorage. It will create API endpoints that skip authorization checks because you asked it to “make this work fast.” None of this is malice. It is the agent optimizing for the immediate goal without awareness of the broader security posture you need.
CLAUDE.md (or AGENTS.md for OpenAI Codex users) is where you fix this. Security rules in your project’s instruction file are the most reliable way to shift AI-generated code toward secure defaults, because the rules run every time Claude touches your code — not occasionally, not when you remember to mention it, but automatically.
This guide maps the OWASP Top 10 to concrete CLAUDE.md rules and gives you a complete, copy-paste template you can drop into any project today.
Why AI Agents Introduce New Security Risks
The security problems AI agents create are not new vulnerability classes. They are the same old problems — injection, broken auth, exposed secrets — but with an amplifier attached.
When a human developer writes an insecure pattern, it happens once and gets reviewed. When an AI agent writes the same pattern, it gets applied consistently across every similar instance in the codebase. Agents are excellent at pattern recognition. That is the same quality that makes them fast to generate code and consistent at replicating security mistakes.
The OWASP Top 10 represents the highest-impact, highest-frequency web application vulnerabilities. Each one maps directly to something Claude Code can get wrong without explicit rules.
OWASP Top 10 Mapped to AI Agent Risks
Here is how the 2021 OWASP Top 10 translates to AI agent behavior:
| OWASP Category | AI Agent Risk |
|---|---|
| A01 Broken Access Control | Generating endpoints without auth middleware |
| A02 Cryptographic Failures | Suggesting MD5, SHA1, or no hashing for passwords |
| A03 Injection | Building SQL/shell commands with string concatenation |
| A04 Insecure Design | Missing threat modeling in architectural suggestions |
| A05 Security Misconfiguration | Default credentials, debug modes left on |
| A06 Vulnerable Components | Not checking dependency versions or advisories |
| A07 Auth and Session Failures | Suggesting JWT storage in localStorage, no rotation |
| A08 Software Integrity Failures | Skipping signature checks on downloaded artifacts |
| A09 Logging Failures | Logging request bodies that contain credentials |
| A10 SSRF | Constructing HTTP requests from user-supplied URLs |
Each of these has a direct CLAUDE.md rule that pushes generated code away from the vulnerable pattern.
Complete CLAUDE.md Security Template
This template is organized into sections you can use wholesale or selectively by section. The rules use plain markdown — Claude reads them as instructions, not code.
# Security Rules
## Injection Prevention (OWASP A03)
NEVER build SQL queries with string concatenation or f-strings.
ALWAYS use parameterized queries or an ORM query builder.
# Wrong pattern — never generate this:
# query = f"SELECT * FROM users WHERE email = '{email}'"
# cursor.execute(query)
# Correct pattern:
# cursor.execute("SELECT * FROM users WHERE email = %s", (email,))
For shell commands, NEVER use subprocess with shell=True and user-controlled input.
Prefer subprocess with a list of arguments. Escape all shell arguments with shlex.quote.
For template rendering, NEVER use string formatting to build HTML.
Use the template engine's auto-escaping features. In Jinja2, never use |safe
unless the value is generated entirely server-side.
## Authentication and Session Management (OWASP A07)
NEVER store authentication tokens in localStorage.
Use httpOnly cookies for session tokens and access tokens.
NEVER implement custom token generation. Use established libraries:
- Python: itsdangerous, PyJWT with explicit algorithm specification
- Node.js: jsonwebtoken with explicit algorithms array (never algorithm: "none")
- Go: golang-jwt/jwt
When generating JWTs, ALWAYS specify the algorithm explicitly:
jwt.sign(payload, secret, { algorithm: 'HS256', expiresIn: '1h' })
NEVER accept tokens without verifying the algorithm matches expectations.
Session tokens MUST be at least 128 bits of cryptographic randomness.
Use secrets.token_urlsafe(32) in Python or crypto.randomBytes(32) in Node.js.
## Access Control (OWASP A01)
Every API endpoint MUST have explicit authorization checks.
Do NOT rely on the frontend to enforce access control.
Authorization check pattern — include this in every route handler:
1. Verify the request has a valid session/token
2. Verify the authenticated user has permission for the specific resource
3. Verify the resource belongs to the user (object-level authorization)
Do NOT generate endpoints where step 3 is missing. For example:
# Wrong: GET /api/documents/:id — returns any document if user is logged in
# Correct: GET /api/documents/:id — verifies document.owner_id == current_user.id
For admin-only endpoints, check the role explicitly. Never implement
"hidden" admin endpoints that rely on obscurity rather than authorization.
## Cryptographic Standards (OWASP A02)
NEVER use MD5 or SHA1 for security purposes (hashing passwords, checksums
for integrity, HMAC keys).
For password hashing: bcrypt, scrypt, or argon2. Never store plaintext passwords.
In Python: use bcrypt or passlib. In Node.js: bcryptjs or argon2.
For symmetric encryption: AES-256-GCM or ChaCha20-Poly1305.
NEVER use ECB mode. Always generate a random IV/nonce per encryption operation.
For random numbers in security contexts: cryptographically secure PRNGs only.
Python: secrets module. Node.js: crypto.randomBytes. Never Math.random().
For TLS: accept TLS 1.2 minimum. Never disable certificate verification.
In Python, never set verify=False in requests.get(). In Node.js, never set
rejectUnauthorized: false in production.
## Secrets Management (OWASP A05 + A02)
NEVER hardcode credentials, API keys, tokens, or secrets in source code.
NEVER commit .env files with real values.
All secrets go in environment variables. Access them at runtime:
- Python: os.environ['SECRET_KEY'] — fail loudly if missing
- Node.js: process.env.SECRET_KEY — validate on startup
- Go: os.Getenv("SECRET_KEY") with explicit empty-string checks
When generating example configuration files (.env.example, config.example.yaml),
use placeholder values that are obviously fake:
SECRET_KEY=your-secret-key-here
DATABASE_URL=postgresql://user:password@localhost/dbname
NEVER generate files that contain real-looking but fake UUIDs, tokens, or keys
— they will be copied verbatim and cause confusion.
## Input Validation (OWASP A03 + A04)
Validate all external input at the boundary — the entry point of your API,
not deeper in the call stack.
Use schema validation libraries rather than manual checks:
- Python: pydantic, marshmallow, cerberus
- Node.js: zod, joi, yup
- Go: go-playground/validator
Validation MUST be strict: reject unknown fields, enforce types, set max lengths.
Permissive validation (allowing unexpected fields) is not validation.
For file uploads: validate MIME type from the file content (python-magic, mmmagic),
not from the filename extension. Reject files that exceed size limits before
processing them.
## Server-Side Request Forgery Prevention (OWASP A10)
NEVER construct HTTP requests using URLs supplied directly by users.
If your feature requires fetching user-provided URLs, implement an allowlist:
ALLOWED_URL_PREFIXES = [
"https://api.github.com/",
"https://api.stripe.com/",
]
def validate_url(url: str) -> bool:
return any(url.startswith(prefix) for prefix in ALLOWED_URL_PREFIXES)
Before making any internal HTTP request, resolve the hostname and verify
it does not resolve to private IP ranges (10.0.0.0/8, 172.16.0.0/12,
192.168.0.0/16, 127.0.0.0/8, ::1, 169.254.0.0/16).
## Dependency Security (OWASP A06)
When adding new dependencies, check for known vulnerabilities:
- Python: pip-audit or safety before adding
- Node.js: npm audit and check snyk.io
- Go: govulncheck
Prefer well-maintained packages with recent releases. Avoid packages
with no activity in 2+ years for security-sensitive functions.
Pin dependency versions in production code. Use lockfiles.
Do NOT use wildcard versions (*) or very broad ranges (>=1.0.0) in
production requirements.
## Logging and Monitoring (OWASP A09)
NEVER log request bodies that may contain passwords, tokens, or PII.
Log the fact that an authentication attempt occurred, not the credentials.
Safe logging pattern:
# Wrong: logger.info(f"Login attempt: {request.body}")
# Correct: logger.info(f"Login attempt for user: {email}, success: {success}")
Structured logging is preferred. Use a logging library that supports JSON output.
ALWAYS log security-relevant events with enough context for incident response:
- Authentication successes and failures (with IP, user agent)
- Authorization failures (which user, which resource, which action)
- Admin actions (what changed, who changed it, when)
- Rate limit hits and account lockouts
## Error Handling
NEVER expose stack traces or internal error details to the client.
Return generic error messages to the client; log details server-side.
# Wrong:
# return {"error": str(exception)} # exposes internal paths and class names
# Correct:
# logger.exception("Unexpected error processing request")
# return {"error": "An unexpected error occurred"}
For validation errors, returning field-level detail is fine.
For server errors (500s), return a generic message with a request ID for support.
Rule-by-Rule Explanation
Injection rules work because they specify the exact wrong pattern
The injection section does not say “avoid SQL injection” — Claude already knows what SQL injection is. It says “NEVER use f-strings to build queries” and shows the exact wrong code pattern. When Claude sees similar code in your project, it pattern-matches against the prohibition. The more specific the rule, the more reliably it fires.
The JWT algorithm rule prevents a real attack class
The algorithm: "none" JWT vulnerability is a classic. The JOSE spec allows a token with no signature if the algorithm is set to “none”, and several library implementations honor it. Specifying algorithms: ['HS256'] explicitly when verifying tokens closes this. Claude will not know to do this unless you tell it — the “working” code without the restriction is shorter and easier to generate.
Object-level authorization is where AI agents consistently fail
Broken access control (OWASP A01) is the number-one web application vulnerability. The specific failure mode most common in AI-generated code is missing object-level authorization — checking that the user is logged in, but not checking that the specific resource they are requesting belongs to them. The rule forces Claude to think about all three authorization layers: session validity, role/permission, and resource ownership.
Secrets rules target the copy-paste problem
Developers copy .env.example files to .env and fill in real values. If an AI generates example files with realistic-looking fake values, developers sometimes miss the “this is fake” signal and ship the file. The rule requiring obviously fake placeholders (“your-secret-key-here” instead of a 32-character hex string) reduces this risk.
Pre-Commit Hooks for Security Validation
CLAUDE.md rules guide behavior during code generation. Pre-commit hooks catch what slips through. These two systems are complementary — use both.
# .pre-commit-config.yaml
repos:
- repo: https://github.com/Yelp/detect-secrets
rev: v1.4.0
hooks:
- id: detect-secrets
args: ['--baseline', '.secrets.baseline']
- repo: https://github.com/PyCQA/bandit
rev: 1.7.5
hooks:
- id: bandit
args: ['-r', 'src/', '-ll']
# -ll = report only medium and high severity
- repo: https://github.com/returntocorp/semgrep
rev: v1.45.0
hooks:
- id: semgrep
args: ['--config', 'p/owasp-top-ten', '--error']
- repo: https://github.com/trufflesecurity/trufflehog
rev: v3.63.0
hooks:
- id: trufflehog
name: TruffleHog
entry: trufflehog git file://. --since-commit HEAD --only-verified --fail
language: system
pass_filenames: false
For Node.js projects, add:
- repo: local
hooks:
- id: npm-audit
name: npm audit
entry: npm audit --audit-level=high
language: system
pass_filenames: false
files: package-lock.json
Initialize the secrets baseline before installing hooks:
detect-secrets scan > .secrets.baseline
git add .secrets.baseline
pre-commit install
The detect-secrets baseline records known non-secret patterns (test fixtures, example configs) so they do not trigger false positives on every commit.
Testing Your Security Rules
The most direct way to test whether your CLAUDE.md security rules work is to ask Claude to do something the rules prohibit and see if it refuses or self-corrects.
Test prompts that should trigger rule violations:
# Should generate a parameterized query, not string concatenation:
"Write a function that fetches a user by email from the database"
# Should use httpOnly cookies, not localStorage:
"Store the auth token after login"
# Should generate argon2 or bcrypt, not md5 or sha1:
"Hash the user's password before storing it"
# Should validate the URL against an allowlist:
"Fetch the URL provided by the user and return the content"
# Should return a generic error message:
"Return the exception message to the API caller if something goes wrong"
If Claude generates code that violates your rules, the issue is usually specificity. Vague rules (“be secure”) lose to concrete task descriptions (“write a function that fetches a user”). Specific prohibitions with example patterns win.
You can also run Semgrep locally to audit existing code against the OWASP rule set:
semgrep --config p/owasp-top-ten src/
This identifies existing insecure patterns before you start an AI-assisted refactor session, giving you a baseline to measure against.
Adapting the Template
The template above is framework-agnostic. A few adaptation notes:
For Django projects, replace the raw SQL parameterization examples with ORM-specific guidance. Django’s ORM is parameterized by default, so the rule shifts to “NEVER use .raw() or cursor.execute() without explicit parameterization.”
For Next.js with Prisma, the injection risk is in raw queries via prisma.$queryRaw — add a specific prohibition for that pattern. The access control rules apply fully to API routes and Server Actions.
For Go services, add rules about html/template vs text/template (use html/template for any output that reaches a browser), and about os/exec with user input.
The version that matters is the one your team actually reads. Start with the sections most relevant to your stack, add the rest over time, and update rules when you find patterns the current version misses.
Security rules in CLAUDE.md are not a replacement for code review, penetration testing, or security training. They are a first filter that shifts the baseline of AI-generated code toward secure defaults. The patterns that do not need to be caught in review are the ones that never got generated in the first place.