security: add prompt injection guards, path traversal prevention, and input validation

Defense-in-depth security hardening for a codebase where markdown files become
LLM system prompts. Adds centralized security module, PreToolUse hook for
injection detection, and CI-ready codebase scan.

New files:
- security.cjs: path traversal prevention, prompt injection scanner/sanitizer,
  safe JSON parsing, field name validation, shell arg validation
- gsd-prompt-guard.js: PreToolUse hook scans .planning/ writes for injection
- security.test.cjs: 62 unit tests for all security functions
- prompt-injection-scan.test.cjs: CI scan of all agent/workflow/command files

Hardened code paths:
- readTextArgOrFile: path traversal guard (--prd, --text-file)
- cmdStateUpdate/Patch: field name validation prevents regex injection
- cmdCommit: sanitizeForPrompt strips invisible chars from commit messages
- gsd-tools --fields: safeJsonParse wraps unprotected JSON.parse
- cmdFrontmatterGet/Set: null byte rejection
- cmdVerifyPathExists: null byte rejection
- install.js: registers prompt guard hook, updates uninstaller

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-03-20 11:38:26 -04:00
parent 5adbba81b2
commit 62db008570
12 changed files with 1283 additions and 4 deletions

View File

@@ -3123,7 +3123,7 @@ function uninstall(isGlobal, runtime = 'claude') {
// 4. Remove GSD hooks
const hooksDir = path.join(targetDir, 'hooks');
if (fs.existsSync(hooksDir)) {
const gsdHooks = ['gsd-statusline.js', 'gsd-check-update.js', 'gsd-check-update.sh', 'gsd-context-monitor.js'];
const gsdHooks = ['gsd-statusline.js', 'gsd-check-update.js', 'gsd-check-update.sh', 'gsd-context-monitor.js', 'gsd-prompt-guard.js'];
let hookCount = 0;
for (const hook of gsdHooks) {
const hookPath = path.join(hooksDir, hook);
@@ -3214,6 +3214,27 @@ function uninstall(isGlobal, runtime = 'claude') {
}
}
// Remove GSD hooks from PreToolUse (prompt injection guard)
if (settings.hooks && settings.hooks.PreToolUse) {
const before = settings.hooks.PreToolUse.length;
settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(entry => {
if (entry.hooks && Array.isArray(entry.hooks)) {
const hasGsdHook = entry.hooks.some(h =>
h.command && h.command.includes('gsd-prompt-guard')
);
return !hasGsdHook;
}
return true;
});
if (settings.hooks.PreToolUse.length < before) {
settingsModified = true;
console.log(` ${green}✓${reset} Removed prompt injection guard hook from settings`);
}
if (settings.hooks.PreToolUse.length === 0) {
delete settings.hooks.PreToolUse;
}
}
// Clean up empty hooks object
if (settings.hooks && Object.keys(settings.hooks).length === 0) {
delete settings.hooks;
@@ -4007,6 +4028,9 @@ function install(isGlobal, runtime = 'claude') {
const contextMonitorCommand = isGlobal
? buildHookCommand(targetDir, 'gsd-context-monitor.js')
: 'node ' + dirName + '/hooks/gsd-context-monitor.js';
const promptGuardCommand = isGlobal
? buildHookCommand(targetDir, 'gsd-prompt-guard.js')
: 'node ' + dirName + '/hooks/gsd-prompt-guard.js';
// Enable experimental agents for Gemini CLI (required for custom sub-agents)
if (isGemini) {
@@ -4086,6 +4110,30 @@ function install(isGlobal, runtime = 'claude') {
}
}
}
// Configure PreToolUse hook for prompt injection detection
const preToolEvent = 'PreToolUse';
if (!settings.hooks[preToolEvent]) {
settings.hooks[preToolEvent] = [];
}
const hasPromptGuardHook = settings.hooks[preToolEvent].some(entry =>
entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-prompt-guard'))
);
if (!hasPromptGuardHook) {
settings.hooks[preToolEvent].push({
matcher: 'Write|Edit',
hooks: [
{
type: 'command',
command: promptGuardCommand,
timeout: 5
}
]
});
console.log(` ${green}✓${reset} Configured prompt injection guard hook`);
}
}
return { settingsPath, settings, statuslineCommand, runtime };