Security by Design
Secrets management, PII handling, permissions, compliance, and safe use of agent tools — built in from the spec, not patched on after a breach.
Security failures in AI-assisted codebases have a specific character: the agent produces code that works correctly for the happy path and handles security incorrectly at the edges. Not because agents are careless — because security constraints are almost never explicit in the spec or the task prompt.
The fix is the same as for observability: make security requirements explicit before any code is written.
Agents will follow the path of least resistance
An agent asked to “add authentication to the API” will add authentication. It will probably not handle token expiry edge cases, add rate limiting, enforce principle of least privilege on the returned data, or sanitise inputs for injection. These require explicit instructions.
Secrets management
The most common and most preventable security failure: secrets in code.
Rules — no exceptions:
- Secrets never appear in source code, ever
- Secrets never appear in commit history, ever
.envfiles never get committed (add to.gitignoreimmediately)- Secrets are accessed through environment variables or a secrets manager, never hardcoded
AGENTS.md rules for secrets
## Secrets and credentials
- NEVER hardcode API keys, tokens, passwords, or connection strings in source code
- NEVER commit .env files — they are git-ignored
- Access secrets via process.env.SECRET_NAME or the configured secrets manager
- Configuration objects must read from environment variables, not contain literal values
- If a secret is needed in a test, use a test-specific environment variable — never the real secret
Detecting committed secrets
Add git-secrets or gitleaks to your CI pipeline to reject commits containing secret patterns before they reach the repository.
PII handling
Personally Identifiable Information (PII) — names, emails, phone numbers, addresses, payment data — requires explicit handling rules. Without them, agents will log it, store it without expiry, and return it in API responses unnecessarily.
Design documents must state:
- What PII this feature handles
- Where it’s stored and for how long
- What’s returned in API responses (principle of least data)
- What’s excluded from logs
## PII in this feature
- Email addresses: stored in users table, never logged, never returned in list endpoints
- Payment data: never stored — we pass tokens to Stripe, store only the last 4 digits
- PII excluded from all log statements
- API responses follow minimum necessary data principle
AGENTS.md rules for PII
## PII rules
- NEVER log email addresses, phone numbers, payment data, or auth tokens
- API responses must not include more PII than the endpoint needs
- Use masked or tokenised values in logs when reference to PII is unavoidable
(e.g., user_id instead of email, last4 instead of full card number)
- Retention: document how long PII is stored and add cleanup mechanisms
Authentication and authorisation
Agents implement authentication correctly when it’s specified. What they tend to miss: authorisation — the distinction between “authenticated user” and “user authorised for this specific resource.”
Common agent failure:
// ❌ Authentication only — is the user logged in?
async getOrder(req: AuthenticatedRequest): Promise<Order> {
return this.orderRepository.findById(req.params.orderId);
}
// ✓ Authentication + authorisation — is this user allowed to see this order?
async getOrder(req: AuthenticatedRequest): Promise<Order> {
const order = await this.orderRepository.findById(req.params.orderId);
if (!order.belongsTo(req.userId)) {
throw new ForbiddenError('Order does not belong to authenticated user');
}
return order;
}
AGENTS.md rules for authorisation
## Authorisation rules
- Authentication (is the user logged in?) is necessary but not sufficient
- Every endpoint that returns or modifies a resource must verify the authenticated
user is authorised for that specific resource
- Authorisation checks go in use cases, not controllers
- Use typed error classes: AuthenticationError vs AuthorisationError vs NotFoundError
- Return 404 (not 403) when a resource exists but the user isn't authorised —
don't confirm existence of resources the user can't access
Input validation
Agents implement input validation when asked. Without explicit instructions, they tend to validate field presence but not field safety.
// ❌ Presence check only
if (!req.body.email) throw new ValidationError('Email required');
// ✓ Presence + safety
const email = EmailAddress.parse(req.body.email); // validates format + normalises
const name = sanitise(req.body.name); // strips HTML/injection vectors
AGENTS.md rule:
## Input validation
- Validate all external input at system boundaries (controllers, CLI handlers, queue consumers)
- Validate presence AND format AND safety — not just presence
- Use value objects to validate and normalise domain inputs (EmailAddress, PhoneNumber, etc.)
- Never pass raw user input to SQL queries — use parameterised queries only
- Never pass raw user input to shell commands
- Sanitise strings that will be rendered in HTML contexts
Agent tool permissions
When agents have access to tools (file system, shell, external APIs), they need explicit permission boundaries. Without them, an agent asked to “update the configuration” might modify files it shouldn’t, or an agent running a query might touch production data.
Define tool permissions in your CLAUDE.md or equivalent:
## Tool permissions
### Allowed
- Read any file in the project directory
- Run tests: bun test, bun run test:*
- Run linting: bun run lint
- Create and edit files within src/, tests/, docs/
- Run database migrations in test environment only
### Forbidden
- NEVER modify files outside the project directory
- NEVER run database migrations against production
- NEVER make HTTP requests to external APIs (use mocks in tests)
- NEVER read or write to /config/secrets/
- NEVER execute shell commands not in the allowed list above
- NEVER access environment variables not listed in .env.example
Least privilege for agents
Apply the principle of least privilege to agent tool access exactly as you would to a service account. An agent that only needs to run tests doesn’t need write access to the infrastructure configuration.
Dependency security
Agents will add dependencies without auditing them. Add npm audit or equivalent to CI and make it a required check:
# In CI pipeline
bun audit --audit-level=high # fail on high or critical vulnerabilities
Also enforce dependency pinning — agents tend to add packages with ^ versions. In production services, prefer exact versions or lock files:
## Dependency rules
- All dependencies must be pinned to exact versions in package.json
- Run `bun audit` before committing new dependencies
- Never add dependencies with known critical vulnerabilities
- Minimise dependencies — if the standard library or an existing dependency can do it, use that
Security checklist for design review
Before implementation starts, verify the design document answers:
- What secrets does this feature need? How are they accessed?
- What PII does this feature handle? Where is it stored? What’s excluded from logs?
- What authentication does this feature require?
- What authorisation checks are needed (per-resource, not just per-user)?
- What external input does this feature accept? How is it validated?
- What tool permissions does the agent need to implement this?