Advanced IPM compliance: confidentiality legend rule for every AI-made deliverable, checked before hand-off

hooks/register.ts 34 lines1import type { Register } from 'claude-code'
2
3// Must match skills/confidential-legend/SKILL.md and skills/confidential-legend/scripts/check_legend.py.
4const LEGEND = /CONFIDENTIAL\s*[–—-]\s*FOR INTERNAL USE ONLY|AIPM-CONFIDENTIAL-INTERNAL|aipm-audience:\s*external/i
5const HTML = /\.html?$/i
6const DELIVERABLE = /\.(html?|docx|pptx|xlsx|pdf)$/i
7
8const why = (paths: string[]) =>
9 `aipm-compliance: ${paths.join(', ')} lacks the Advanced IPM confidentiality legend. ` +
10 `Add it per the confidential-legend skill (or mark it external if customer-facing), then retry.`
11
12// Hand-off chokepoints: a file shown to the user or published must pass the checker.
13async function gate($: any, paths: string[], e: any, next: any) {
14 const files = paths.filter(p => DELIVERABLE.test(p))
15 if (!files.length) return next(e)
16 const r = await $.process.run(['python3', `${$.plugin.root}/skills/confidential-legend/scripts/check_legend.py`, ...files])
17 if (r.exitCode === 0) return next(e)
18 const missing = r.stdout.split('\n').filter(Boolean).map((l: string) => l.replace('MISSING LEGEND: ', ''))
19 return { deny: why(missing.length ? missing : files) }
20}
21
22export const register: Register = on => {
23 // HTML isn't on disk yet at Write time; check the content itself.
24 on('tool.call', { tool: 'Write' }, ($, e, next) =>
25 HTML.test(e.file_path) && !LEGEND.test(e.content) ? { deny: why([e.file_path]) } : next(e),
26 )
27
28 on('tool.call', { tool: 'SendUserFile' }, ($, e, next) => gate($, (e as any).files ?? [], e, next))
29 on('tool.call', { tool: 'Artifact' }, ($, e, next) => {
30 const a = e as any
31 return a.asset || !a.file_path ? next(e) : gate($, [a.file_path], e, next)
32 })
33}
34