diff --git a/.claude/hooks/continuous_save.py b/.claude/hooks/continuous_save.py index b02d819d..2aa84613 100644 --- a/.claude/hooks/continuous_save.py +++ b/.claude/hooks/continuous_save.py @@ -289,7 +289,7 @@ def on_user_message(content: str, metadata: Dict = None): """Handler para mensagem do usuário (UserPromptSubmit).""" entry = { 'type': 'user_message', - 'content': content[:2000], # Limitar tamanho + 'content': content[:200], # Limitar tamanho (security: avoid logging sensitive data) 'metadata': metadata or {} } @@ -304,11 +304,20 @@ def on_user_message(content: str, metadata: Dict = None): def on_tool_use(tool_name: str, tool_input: Dict = None, result_preview: str = None): """Handler para uso de ferramenta (PostToolUse).""" + # Security: log only metadata, not full content (may contain secrets/tokens) + safe_input = None + if tool_input: + # Only log file paths and tool name, not content + safe_input = {} + for key in ['file_path', 'filepath', 'path', 'command', 'pattern']: + if key in tool_input: + safe_input[key] = str(tool_input[key])[:200] + entry = { 'type': 'tool_use', 'tool': tool_name, - 'input_preview': str(tool_input)[:500] if tool_input else None, - 'result_preview': result_preview[:500] if result_preview else None + 'input_preview': str(safe_input)[:200] if safe_input else None, + 'result_preview': result_preview[:200] if result_preview else None } # Extrair arquivo se for Edit/Write/Read @@ -325,7 +334,7 @@ def on_response_complete(preview: str = None): """Handler para resposta completa (Stop).""" entry = { 'type': 'response', - 'preview': preview[:500] if preview else '[response complete]' + 'preview': preview[:200] if preview else '[response complete]' } append_to_jsonl(entry) diff --git a/.claude/hooks/gsd-check-update.js b/.claude/hooks/gsd-check-update.js index df3cd228..e831c619 100644 --- a/.claude/hooks/gsd-check-update.js +++ b/.claude/hooks/gsd-check-update.js @@ -1,6 +1,7 @@ #!/usr/bin/env node // Check for GSD updates in background, write result to cache // Called by SessionStart hook - runs once per session +// NOTE: This is the ONLY hook with network access (npm registry query) const fs = require('fs'); const path = require('path'); @@ -22,9 +23,10 @@ if (!fs.existsSync(cacheDir)) { } // Run check in background (spawn background process, windowsHide prevents console flash) +// Security: uses execFileSync (no shell) with explicit timeout for network call const child = spawn(process.execPath, ['-e', ` const fs = require('fs'); - const { execSync } = require('child_process'); + const { execFileSync } = require('child_process'); const cacheFile = ${JSON.stringify(cacheFile)}; const projectVersionFile = ${JSON.stringify(projectVersionFile)}; @@ -42,8 +44,14 @@ const child = spawn(process.execPath, ['-e', ` let latest = null; try { - latest = execSync('npm view get-shit-done-cc version', { encoding: 'utf8', timeout: 10000, windowsHide: true }).trim(); - } catch (e) {} + latest = execFileSync('npm', ['view', 'get-shit-done-cc', 'version'], { + encoding: 'utf8', + timeout: 10000, + windowsHide: true + }).trim(); + } catch (e) { + // Network or npm failure - non-critical, write cache with error state + } const result = { update_available: latest && installed !== latest, @@ -52,7 +60,11 @@ const child = spawn(process.execPath, ['-e', ` checked: Math.floor(Date.now() / 1000) }; - fs.writeFileSync(cacheFile, JSON.stringify(result)); + try { + fs.writeFileSync(cacheFile, JSON.stringify(result)); + } catch (e) { + // Cache write failure - non-critical + } `], { stdio: 'ignore', windowsHide: true, diff --git a/.claude/hooks/memory_updater.py b/.claude/hooks/memory_updater.py index bec6fb2f..4c22230e 100644 --- a/.claude/hooks/memory_updater.py +++ b/.claude/hooks/memory_updater.py @@ -86,6 +86,8 @@ def update_memory_file(section, content): with open(memory_path, 'r', encoding='utf-8') as f: memory = f.read() + original_memory = memory # snapshot for audit diff + # Encontrar secao de decisoes e adicionar if section == 'decisions': # Procurar por "### Decisoes Importantes" @@ -128,6 +130,15 @@ def update_memory_file(section, content): with open(memory_path, 'w', encoding='utf-8') as f: f.write(memory) + # Audit: log what changed + if memory != original_memory: + diff_lines = len(memory.splitlines()) - len(original_memory.splitlines()) + audit_file_modification( + str(memory_path), + f'update_section:{section}', + f'+{diff_lines} lines, content: {content[:100]}' + ) + return True def log_update(update_type, content): @@ -145,6 +156,25 @@ def log_update(update_type, content): with open(log_path, 'a', encoding='utf-8') as f: f.write(json.dumps(log_entry, ensure_ascii=False) + '\n') +def audit_file_modification(file_path, modification_type, diff_summary): + """Audit log for ALL file modifications made by this hook.""" + project_dir = get_project_dir() + audit_path = Path(project_dir) / 'logs' / 'memory-audit.jsonl' + audit_path.parent.mkdir(parents=True, exist_ok=True) + + audit_entry = { + 'timestamp': datetime.now().isoformat(), + 'file_modified': str(file_path), + 'modification_type': modification_type, + 'diff_summary': diff_summary[:500] + } + + try: + with open(audit_path, 'a', encoding='utf-8') as f: + f.write(json.dumps(audit_entry, ensure_ascii=False) + '\n') + except Exception: + pass # Audit logging should never block the hook + def main(): """Funcao principal do hook.""" try: diff --git a/.claude/hooks/notification_system.py b/.claude/hooks/notification_system.py index fcd6d61f..b4b596f3 100644 --- a/.claude/hooks/notification_system.py +++ b/.claude/hooks/notification_system.py @@ -35,10 +35,17 @@ LOGS_PATH = PROJECT_ROOT / 'logs' / 'notifications' LOGS_PATH.mkdir(parents=True, exist_ok=True) +def _escape_applescript(s: str) -> str: + """Escape string for safe use inside AppleScript double-quoted strings.""" + return s.replace('\\', '\\\\').replace('"', '\\"') + def send_macos_notification(title: str, message: str, sound: str = "default"): """Send native macOS notification""" + safe_message = _escape_applescript(message) + safe_title = _escape_applescript(title) + safe_sound = _escape_applescript(sound) script = f''' - display notification "{message}" with title "{title}" sound name "{sound}" + display notification "{safe_message}" with title "{safe_title}" sound name "{safe_sound}" ''' try: subprocess.run(['osascript', '-e', script], capture_output=True, timeout=5)