Defense-in-Depth Validation
Overview
When you fix a bug caused by invalid data, adding validation at one place feels sufficient. But that single check can be bypassed by different code paths, refactoring, or mocks.
Core principle: Validate at EVERY layer data passes through. Make the bug structurally impossible.
Why Multiple Layers
Single validation: "We fixed the bug" Multiple layers: "We made the bug impossible"
Different layers catch different cases:
-
Entry validation catches most bugs
-
Business logic catches edge cases
-
Environment guards prevent context-specific dangers
-
Debug logging helps when other layers fail
The Four Layers
Layer 1: Entry Point Validation
Purpose: Reject obviously invalid input at API boundary
function createProject(name: string, workingDirectory: string) {
if (!workingDirectory || workingDirectory.trim() === '') {
throw new Error('workingDirectory cannot be empty');
}
if (!existsSync(workingDirectory)) {
throw new Error(workingDirectory does not exist: ${workingDirectory});
}
if (!statSync(workingDirectory).isDirectory()) {
throw new Error(workingDirectory is not a directory: ${workingDirectory});
}
// ... proceed
}
Layer 2: Business Logic Validation
Purpose: Ensure data makes sense for this operation
function initializeWorkspace(projectDir: string, sessionId: string) { if (!projectDir) { throw new Error('projectDir required for workspace initialization'); } // ... proceed }
Layer 3: Environment Guards
Purpose: Prevent dangerous operations in specific contexts
async function gitInit(directory: string) { // In tests, refuse git init outside temp directories if (process.env.NODE_ENV === 'test') { const normalized = normalize(resolve(directory)); const tmpDir = normalize(resolve(tmpdir()));
if (!normalized.startsWith(tmpDir)) {
throw new Error(
`Refusing git init outside temp dir during tests: ${directory}`
);
}
} // ... proceed }
Layer 4: Debug Instrumentation
Purpose: Capture context for forensics
async function gitInit(directory: string) { const stack = new Error().stack; logger.debug('About to git init', { directory, cwd: process.cwd(), stack, }); // ... proceed }
Applying the Pattern
When you find a bug:
-
Trace the data flow - Where does bad value originate? Where used?
-
Map all checkpoints - List every point data passes through
-
Add validation at each layer - Entry, business, environment, debug
-
Test each layer - Try to bypass layer 1, verify layer 2 catches it
Example from Session
Bug: Empty projectDir caused git init in source code
Data flow:
-
Test setup → empty string
-
Project.create(name, '')
-
WorkspaceManager.createWorkspace('')
-
git init runs in process.cwd()
Four layers added:
-
Layer 1: Project.create() validates not empty/exists/writable
-
Layer 2: WorkspaceManager validates projectDir not empty
-
Layer 3: WorktreeManager refuses git init outside tmpdir in tests
-
Layer 4: Stack trace logging before git init
Result: All 1847 tests passed, bug impossible to reproduce
Key Insight
All four layers were necessary. During testing, each layer caught bugs the others missed:
-
Different code paths bypassed entry validation
-
Mocks bypassed business logic checks
-
Edge cases on different platforms needed environment guards
-
Debug logging identified structural misuse
Don't stop at one validation point. Add checks at every layer.