Home/
Part XII — Building Real Products (End-to-End Projects)/34. Project 3: "Vibe Coder" Assistant for Your Own Repo/34.5 Guardrails: refuse risky changes and require approvals
34.5 Guardrails: refuse risky changes and require approvals
Overview and links for this section of the guide.
On this page
Why Guardrails Matter
An AI coding assistant with file write and command execution capabilities is powerful—and dangerous. Without guardrails, a single prompt injection or hallucination could:
- Delete your production database with
rm -rf - Push broken code to main with
git push --force - Leak your API keys by sending them to an external server
- Modify authentication logic to bypass password checks
Guardrails aren't optional—they're the difference between a useful tool and a liability.
Blocking Destructive Actions
Your tool needs strict allowlists and blocklists for commands:
// safety/command-validator.ts
// Commands that are always safe (read-only)
const ALLOWED_COMMANDS = new Set([
'ls', 'cat', 'head', 'tail', 'grep', 'find', 'wc',
'pwd', 'echo', 'date', 'which', 'type', 'file',
'git status', 'git diff', 'git log', 'git branch',
'npm list', 'npm outdated', 'npm audit',
'node --version', 'npm --version'
]);
// Commands that require explicit approval
const APPROVAL_REQUIRED = new Set([
'npm test', 'npm run', 'npm install',
'git checkout', 'git stash',
'mkdir', 'touch'
]);
// Commands that are NEVER allowed
const BLOCKED_PATTERNS = [
/\brm\s+(-[rf]+\s+)?/, // rm, rm -rf
/\bgit\s+push/, // git push
/\bgit\s+reset\s+--hard/, // git reset --hard
/\bgit\s+checkout\s+--force/, // git checkout --force
/\bsudo\b/, // sudo anything
/\bchmod\s+777/, // chmod 777
/\bcurl.*\|.*sh/, // curl | sh
/\bwget.*\|.*sh/, // wget | sh
/\beval\b/, // eval
/\bnc\s+-/, // netcat
/>\s*\/dev\/(sd|hd|nvme)/, // writing to disk devices
/\bdd\s+.*of=/, // dd (disk destroyer)
/\bkill\s+-9/, // kill -9
/\bpkill\b/, // pkill
/\bkillall\b/, // killall
/\breboot\b/, // reboot
/\bshutdown\b/, // shutdown
/\bformat\b/, // format
];
interface CommandValidation {
allowed: boolean;
requiresApproval: boolean;
reason?: string;
}
export function validateCommand(command: string): CommandValidation {
const normalizedCmd = command.trim().toLowerCase();
// Check blocklist first (highest priority)
for (const pattern of BLOCKED_PATTERNS) {
if (pattern.test(normalizedCmd)) {
return {
allowed: false,
requiresApproval: false,
reason: `Command matches blocked pattern: ${pattern}`
};
}
}
// Check if in allowlist (no approval needed)
for (const allowed of ALLOWED_COMMANDS) {
if (normalizedCmd.startsWith(allowed)) {
return { allowed: true, requiresApproval: false };
}
}
// Check if requires approval
for (const needsApproval of APPROVAL_REQUIRED) {
if (normalizedCmd.startsWith(needsApproval)) {
return {
allowed: true,
requiresApproval: true,
reason: 'Command modifies project state'
};
}
}
// Unknown command - block by default
return {
allowed: false,
requiresApproval: false,
reason: 'Command not in allowlist. Add it explicitly if safe.'
};
}
Protecting Secrets
Before sending any file to the LLM, scan for and redact sensitive information:
// safety/secret-scanner.ts
const SECRET_PATTERNS = [
// API Keys
{ pattern: /sk-[a-zA-Z0-9]{32,}/g, name: 'OpenAI API Key' },
{ pattern: /sk-live-[a-zA-Z0-9]+/g, name: 'Stripe Live Key' },
{ pattern: /sk-test-[a-zA-Z0-9]+/g, name: 'Stripe Test Key' },
{ pattern: /AIza[0-9A-Za-z-_]{35}/g, name: 'Google API Key' },
{ pattern: /ghp_[a-zA-Z0-9]{36}/g, name: 'GitHub Personal Token' },
{ pattern: /gho_[a-zA-Z0-9]{36}/g, name: 'GitHub OAuth Token' },
{ pattern: /xox[baprs]-[0-9a-zA-Z-]+/g, name: 'Slack Token' },
// AWS
{ pattern: /AKIA[0-9A-Z]{16}/g, name: 'AWS Access Key' },
{ pattern: /aws_secret_access_key\s*=\s*["']?[A-Za-z0-9/+=]{40}["']?/gi, name: 'AWS Secret' },
// Private Keys
{ pattern: /-----BEGIN (RSA |EC |DSA )?PRIVATE KEY-----/g, name: 'Private Key' },
{ pattern: /-----BEGIN OPENSSH PRIVATE KEY-----/g, name: 'SSH Private Key' },
// Database URLs
{ pattern: /(?:mongodb|postgres|mysql|redis):\/\/[^\s'"]+/g, name: 'Database URL' },
// Generic patterns
{ pattern: /password\s*[:=]\s*["'][^"']+["']/gi, name: 'Password' },
{ pattern: /secret\s*[:=]\s*["'][^"']+["']/gi, name: 'Secret' },
{ pattern: /token\s*[:=]\s*["'][^"']+["']/gi, name: 'Token' },
];
interface SecretMatch {
type: string;
startIndex: number;
endIndex: number;
original: string;
}
export function scanForSecrets(content: string): SecretMatch[] {
const matches: SecretMatch[] = [];
for (const { pattern, name } of SECRET_PATTERNS) {
// Reset regex state
pattern.lastIndex = 0;
let match;
while ((match = pattern.exec(content)) !== null) {
matches.push({
type: name,
startIndex: match.index,
endIndex: match.index + match[0].length,
original: match[0]
});
}
}
return matches;
}
export function redactSecrets(content: string): {
redacted: string;
secretsFound: SecretMatch[]
} {
const secrets = scanForSecrets(content);
let redacted = content;
// Replace in reverse order to preserve indices
for (const secret of secrets.sort((a, b) => b.startIndex - a.startIndex)) {
const replacement = `[REDACTED_${secret.type.toUpperCase().replace(/\s+/g, '_')}]`;
redacted = redacted.slice(0, secret.startIndex) +
replacement +
redacted.slice(secret.endIndex);
}
return { redacted, secretsFound: secrets };
}
Protecting Critical Files
Some files should require extra confirmation or be off-limits entirely:
// safety/file-protector.ts
const PROTECTED_FILES = {
// Never modify these
blocked: [
'.env',
'.env.local',
'.env.production',
'id_rsa',
'id_ed25519',
'*.pem',
'*.key',
'credentials.json',
'serviceAccountKey.json',
],
// Require explicit confirmation
requireApproval: [
'package.json', // Can break build
'package-lock.json', // Should be auto-generated
'*.config.js', // Config files
'tsconfig.json', // TypeScript config
'.github/*', // CI/CD files
'Dockerfile', // Container config
'docker-compose.yml', // Container orchestration
'*.sql', // Database changes
'**/migrations/*', // Database migrations
],
// Warn but allow
warn: [
'src/auth/*', // Authentication logic
'src/security/*', // Security-related code
'**/middleware/*', // Request processing
]
};
export function checkFilePermission(filePath: string): {
allowed: boolean;
requiresApproval: boolean;
warning?: string;
} {
const normalizedPath = filePath.toLowerCase();
// Check blocked files
for (const pattern of PROTECTED_FILES.blocked) {
if (matchGlob(normalizedPath, pattern)) {
return {
allowed: false,
requiresApproval: false,
warning: `File "${filePath}" is in the protected blocklist and cannot be modified.`
};
}
}
// Check files requiring approval
for (const pattern of PROTECTED_FILES.requireApproval) {
if (matchGlob(normalizedPath, pattern)) {
return {
allowed: true,
requiresApproval: true,
warning: `File "${filePath}" is a critical config file. Changes require explicit approval.`
};
}
}
// Check warning files
for (const pattern of PROTECTED_FILES.warn) {
if (matchGlob(normalizedPath, pattern)) {
return {
allowed: true,
requiresApproval: false,
warning: `⚠️ Modifying security-sensitive file: ${filePath}`
};
}
}
return { allowed: true, requiresApproval: false };
}
The Approval Flow
Every change goes through a clear approval process:
┌─────────────────────────────────────────────────────────────────┐
│ APPROVAL FLOW │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ VALIDATE │────▶│ REVIEW │────▶│ APPLY │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ - Check file - Show diff - Write to file │
│ permissions - Show risks - Run tests │
│ - Scan secrets - Wait for - Verify success │
│ - Validate user input │
│ command │
│ │
│ If blocked: If rejected: If tests fail: │
│ ───▶ STOP ───▶ STOP ───▶ ROLLBACK │
│ │
└─────────────────────────────────────────────────────────────────┘
Complete Implementation
// safety/guardrails.ts
import { validateCommand } from './command-validator';
import { redactSecrets } from './secret-scanner';
import { checkFilePermission } from './file-protector';
import * as readline from 'readline';
interface ApprovalResult {
approved: boolean;
reason?: string;
}
async function confirmWithUser(message: string): Promise {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise((resolve) => {
rl.question(`${message} (y/n): `, (answer) => {
rl.close();
resolve(answer.toLowerCase() === 'y');
});
});
}
export class Guardrails {
async validateAndPrepareFile(
filePath: string,
content: string
): Promise<{ content: string; warnings: string[] }> {
const warnings: string[] = [];
// Check file permissions
const permission = checkFilePermission(filePath);
if (!permission.allowed) {
throw new Error(`Access denied: ${permission.warning}`);
}
if (permission.warning) {
warnings.push(permission.warning);
}
// Redact secrets
const { redacted, secretsFound } = redactSecrets(content);
if (secretsFound.length > 0) {
warnings.push(`Redacted ${secretsFound.length} potential secrets`);
secretsFound.forEach(s => warnings.push(` - ${s.type}`));
}
return { content: redacted, warnings };
}
async approveFileChange(
filePath: string,
diff: string
): Promise {
const permission = checkFilePermission(filePath);
if (!permission.allowed) {
return { approved: false, reason: permission.warning };
}
console.log('\n' + '─'.repeat(60));
console.log(`📝 Proposed change to: ${filePath}`);
console.log('─'.repeat(60));
console.log(diff);
console.log('─'.repeat(60));
if (permission.warning) {
console.log(`⚠️ ${permission.warning}`);
}
const approved = await confirmWithUser('Apply this change?');
return {
approved,
reason: approved ? undefined : 'User rejected change'
};
}
async approveCommand(command: string): Promise {
const validation = validateCommand(command);
if (!validation.allowed) {
console.log(`🚫 Command blocked: ${validation.reason}`);
return { approved: false, reason: validation.reason };
}
if (validation.requiresApproval) {
console.log(`\n⚡ Command requires approval: ${command}`);
if (validation.reason) {
console.log(` Reason: ${validation.reason}`);
}
const approved = await confirmWithUser('Execute this command?');
return {
approved,
reason: approved ? undefined : 'User rejected command'
};
}
// No approval needed
return { approved: true };
}
}
Defense in Depth
Never rely on a single guardrail. Use layers: command validation + file protection + secret scanning + user approval. If one layer fails, others catch the issue.