#!/usr/bin/env node // UserPromptSubmit hook: records an explicit "Core-Freigabe: [, ]" from the user's message. // The release covers only this message; every new message replaces or removes it. // Only the user's own prompt is read, never tool output or file content. import fs from 'node:fs'; import path from 'node:path'; import { RELEASE_FILE, ALL_AREAS, resolveAreaName } from './core-areas.mjs'; const RELEASE_PHRASE = /core[- ]?freigabe\s*:?\s*/gi; const SEPARATOR = /^(\s*(,|;|\/|&|\+|\bund\b|\band\b)\s*|\s+)/i; // Reads area names from the start of the text after the phrase; trailing prose ends the list. // Any character other than a letter or space (dash, colon, digit, punctuation, newline) ends a name, // so "Core-Freigabe: Shared Kernel – füge ..." releases Shared Kernel. function readAreas(tail, unknown) { const areas = []; let rest = tail; while (rest.length > 0) { const word = rest.match(/^[\p{L}][\p{L} ]*?(?=$|[^\p{L} ]|\s+(und|and)\b)/u)?.[0] ?? ''; const candidates = word.split(' ').map((_, index, parts) => parts.slice(0, parts.length - index).join(' ')); const match = candidates.find((candidate) => resolveAreaName(candidate)); if (!match) { if (areas.length === 0 && word) unknown.push(word.trim()); break; } areas.push(resolveAreaName(match)); rest = rest.slice(match.length); const separator = rest.match(SEPARATOR); if (!separator) break; rest = rest.slice(separator[0].length); } return areas; } function parseRelease(prompt) { const areas = new Set(); const unknown = []; for (const match of prompt.matchAll(RELEASE_PHRASE)) { const tail = prompt.slice(match.index + match[0].length); for (const area of readAreas(tail, unknown)) areas.add(area); } return { areas: [...areas], unknown }; } function label(area) { return area === ALL_AREAS ? 'alle Core-Bereiche ausser Governance' : area; } function main(raw) { const input = JSON.parse(raw); const projectDir = process.env.CLAUDE_PROJECT_DIR || input.cwd || process.cwd(); const releaseFile = path.join(projectDir, RELEASE_FILE); const { areas, unknown } = parseRelease(String(input.prompt ?? '')); fs.rmSync(releaseFile, { force: true }); const output = {}; if (areas.length > 0) { fs.writeFileSync(releaseFile, JSON.stringify({ sessionId: input.session_id ?? null, areas, createdAt: new Date().toISOString() })); const text = areas.map(label).join(', '); output.systemMessage = `✅ CORE-FREIGABE für diese Nachricht: ${text}`; output.hookSpecificOutput = { hookEventName: 'UserPromptSubmit', additionalContext: `The user released these Core areas for this message only: ${text}. Other Core areas stay blocked.`, }; } if (unknown.length > 0) { output.systemMessage = [output.systemMessage, `⚠ Core-Freigabe nicht erkannt: "${unknown.join('", "')}". Gueltige Bereiche: Ingest, Normalization, Evaluation, Store, Disclosure, Reference Catalogs, Shared Kernel, Presentation, Platform, Core Tests, Governance, alle.`] .filter(Boolean).join('\n'); } if (Object.keys(output).length > 0) process.stdout.write(JSON.stringify(output)); } let raw = ''; process.stdin.setEncoding('utf8'); process.stdin.on('data', (chunk) => { raw += chunk; }); process.stdin.on('end', () => { try { main(raw); } catch (error) { // Fail closed: without a readable message no release remains active. try { fs.rmSync(path.join(process.env.CLAUDE_PROJECT_DIR || process.cwd(), RELEASE_FILE), { force: true }); } catch {} process.stdout.write(JSON.stringify({ systemMessage: `⚠ Core-Freigabe konnte nicht gelesen werden (${error instanceof Error ? error.message : 'unbekannter Fehler'}). Der Core bleibt gesperrt.`, })); } });