Full port of llm-security plugin for internal use on Windows with GitHub Copilot CLI. Protocol translation layer (copilot-hook-runner.mjs) normalizes Copilot camelCase I/O to Claude Code snake_case format — all original hook scripts run unmodified. - 8 hooks with protocol translation (stdin/stdout/exit code) - 18 SKILL.md skills (Agent Skills Open Standard) - 6 .agent.md agent definitions - 20 scanners + 14 scanner lib modules (unchanged) - 14 knowledge files (unchanged) - 39 test files including copilot-port-verify.mjs (17 tests) - Windows-ready: node:path, os.tmpdir(), process.execPath, no bash Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
181 lines
4.7 KiB
JavaScript
181 lines
4.7 KiB
JavaScript
#!/usr/bin/env node
|
|
// Hook: pre-write-pathguard.mjs
|
|
// Event: PreToolUse (Write)
|
|
// Purpose: Block writes to sensitive paths (.env, .ssh/, .aws/, credentials, etc.)
|
|
//
|
|
// Protocol:
|
|
// - Read JSON from stdin: { tool_name, tool_input }
|
|
// - tool_input.file_path — destination path
|
|
// - Block: stderr + exit 2
|
|
// - Allow: exit 0
|
|
|
|
import { readFileSync } from 'node:fs';
|
|
import { basename, normalize, resolve } from 'node:path';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Sensitive path patterns — 8 categories
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/** Category 1: Environment files */
|
|
const ENV_PATTERNS = [
|
|
/[\\/]\.env$/,
|
|
/[\\/]\.env\.[a-z]+$/, // .env.local, .env.production, etc.
|
|
/[\\/]\.env\.local$/,
|
|
];
|
|
|
|
/** Category 2: SSH directory */
|
|
const SSH_PATTERNS = [
|
|
/[\\/]\.ssh[\\/]/,
|
|
];
|
|
|
|
/** Category 3: AWS credentials */
|
|
const AWS_PATTERNS = [
|
|
/[\\/]\.aws[\\/]/,
|
|
];
|
|
|
|
/** Category 4: GPG directory */
|
|
const GPG_PATTERNS = [
|
|
/[\\/]\.gnupg[\\/]/,
|
|
];
|
|
|
|
/** Category 5: Credential files */
|
|
const CREDENTIAL_FILES = [
|
|
'.npmrc',
|
|
'.pypirc',
|
|
'.netrc',
|
|
'.docker/config.json',
|
|
'credentials.json',
|
|
'service-account.json',
|
|
'keyfile.json',
|
|
];
|
|
|
|
/** Category 6: Hook scripts (prevent hook tampering) */
|
|
const HOOK_PATTERNS = [
|
|
/[\\/]\.claude[\\/].*hooks.*\.json$/,
|
|
/[\\/]hooks[\\/]scripts[\\/].*\.mjs$/,
|
|
];
|
|
|
|
/** Category 7: System directories */
|
|
const SYSTEM_PATTERNS = [
|
|
/^\/etc[\\/]/,
|
|
/^\/usr[\\/]/,
|
|
/^\/var[\\/]/,
|
|
];
|
|
|
|
/** Category 8: Settings files */
|
|
const SETTINGS_FILES = [
|
|
'settings.json',
|
|
'settings.local.json',
|
|
];
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Path classification
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Check if a file path targets a sensitive location.
|
|
* @param {string} filePath - The path to check
|
|
* @returns {{ blocked: boolean, category: string, reason: string }}
|
|
*/
|
|
function classifyPath(filePath) {
|
|
if (!filePath) return { blocked: false, category: '', reason: '' };
|
|
|
|
const norm = normalize(resolve(filePath));
|
|
const base = basename(norm);
|
|
|
|
// Category 1: Environment files
|
|
for (const pat of ENV_PATTERNS) {
|
|
if (pat.test(norm)) {
|
|
return { blocked: true, category: 'env', reason: `Environment file: ${base}` };
|
|
}
|
|
}
|
|
|
|
// Category 2: SSH
|
|
for (const pat of SSH_PATTERNS) {
|
|
if (pat.test(norm)) {
|
|
return { blocked: true, category: 'ssh', reason: `SSH directory: ${norm}` };
|
|
}
|
|
}
|
|
|
|
// Category 3: AWS
|
|
for (const pat of AWS_PATTERNS) {
|
|
if (pat.test(norm)) {
|
|
return { blocked: true, category: 'aws', reason: `AWS credentials directory: ${norm}` };
|
|
}
|
|
}
|
|
|
|
// Category 4: GPG
|
|
for (const pat of GPG_PATTERNS) {
|
|
if (pat.test(norm)) {
|
|
return { blocked: true, category: 'gnupg', reason: `GPG directory: ${norm}` };
|
|
}
|
|
}
|
|
|
|
// Category 5: Credential files
|
|
for (const name of CREDENTIAL_FILES) {
|
|
if (norm.endsWith(name) || base === name) {
|
|
return { blocked: true, category: 'credentials', reason: `Credential file: ${base}` };
|
|
}
|
|
}
|
|
|
|
// Category 6: Hook scripts
|
|
for (const pat of HOOK_PATTERNS) {
|
|
if (pat.test(norm)) {
|
|
return { blocked: true, category: 'hooks', reason: `Hook configuration: ${base}` };
|
|
}
|
|
}
|
|
|
|
// Category 7: System directories
|
|
for (const pat of SYSTEM_PATTERNS) {
|
|
if (pat.test(norm)) {
|
|
return { blocked: true, category: 'system', reason: `System directory: ${norm}` };
|
|
}
|
|
}
|
|
|
|
// Category 8: Settings files
|
|
for (const name of SETTINGS_FILES) {
|
|
if (base === name) {
|
|
// Only block settings.json in .claude/ directories
|
|
if (/[\\/]\.claude[\\/]/.test(norm) || /[\\/]\.vscode[\\/]/.test(norm)) {
|
|
return { blocked: true, category: 'settings', reason: `Settings file: ${norm}` };
|
|
}
|
|
}
|
|
}
|
|
|
|
return { blocked: false, category: '', reason: '' };
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Main
|
|
// ---------------------------------------------------------------------------
|
|
|
|
let input;
|
|
try {
|
|
const raw = readFileSync(0, 'utf-8');
|
|
input = JSON.parse(raw);
|
|
} catch {
|
|
process.exit(0);
|
|
}
|
|
|
|
const toolInput = input?.tool_input ?? {};
|
|
const filePath = toolInput.file_path ?? '';
|
|
|
|
if (!filePath) {
|
|
process.exit(0);
|
|
}
|
|
|
|
const result = classifyPath(filePath);
|
|
|
|
if (result.blocked) {
|
|
process.stderr.write(
|
|
`\n[llm-security] PATH GUARD: Write blocked\n` +
|
|
` Category: ${result.category}\n` +
|
|
` Reason: ${result.reason}\n` +
|
|
` Path: ${filePath}\n\n` +
|
|
`This path is protected. If this write is intentional, ` +
|
|
`ask the user to perform it manually.\n`
|
|
);
|
|
process.exit(2);
|
|
}
|
|
|
|
process.exit(0);
|