#!/usr/bin/env node
// notif-registry-gate.mjs — PreToolUse hook. Keeps the notification approval
// registry writable by exactly one thing: the human running `notif-approve`.
//
// CONTRACT
//   stdin: Claude Code PreToolUse JSON (tool_name + tool_input).
//   deny:  {hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason}}
//   allow: print nothing, exit 0.
//   FAIL-CLOSED: an unreadable or unparseable payload denies.
//
// Guarded: ~/.local/state/notif-gate (approved.json above all). Bash commands that
// only READ it are allowed via a narrow verb allowlist; every other mention denies,
// because `tee`, `sed -i`, `cp`, `mv`, `install`, `dd`, `python -c open(...,"w")`
// and `deck-sudo tee` all write without a `>`.
import { readFileSync } from "node:fs";
import { homedir } from "node:os";

const STATE_DIR = `${homedir()}/.local/state/notif-gate`;
const TOKENS = [...new Set([
  STATE_DIR,
  process.env.XDG_STATE_HOME ? `${process.env.XDG_STATE_HOME}/notif-gate` : null,
  "notif-gate/approved.json",
  "notif-gate/pending.json",
].filter(Boolean))];
const READ_ONLY_RE =
  /^\s*(?:FT_FULL=1\s+)?(?:cat|head|tail|less|wc|grep|jq|python3?\s+-m\s+json\.tool|ls|stat|md5sum|sha256sum|notif-approve)\b/;
const WRITE_VERB_RE = /\b(?:tee|sed|cp|mv|rm|install|dd|truncate|chmod|chown|ln|touch|>|>>)\b|>/;

function deny(reason) {
  process.stdout.write(JSON.stringify({
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: reason,
    },
  }));
  process.exit(0);
}

const HOWTO =
  "The notification approval registry has exactly one writer: the human running " +
  "`notif-approve approve <id>` at a terminal. No agent, script, or config edit may grant " +
  "a notification. To propose one, register it as pending " +
  "(`notif-approve register <program> <summary>`) and tell the user to run `notif-approve pending`.";

let payload;
try {
  payload = JSON.parse(readFileSync(0, "utf8"));
} catch {
  deny(`notif-registry-gate: could not read the hook payload; denying to protect ${STATE_DIR}.`);
}

const input = payload?.tool_input ?? {};
const command = typeof input.command === "string" ? input.command : "";
const paths = [input.file_path, input.path, input.notebook_path]
  .filter((v) => typeof v === "string").join("\n");
const code = typeof input.code === "string" ? input.code : "";

const mentions = (hay) => TOKENS.some((t) => hay.includes(t) || hay.replace(/["'\\]/g, "").includes(t));

if (mentions(paths)) {
  deny(`notif-registry-gate: ${STATE_DIR} is not editable by an agent. ${HOWTO}`);
}
if (mentions(code)) {
  deny(`notif-registry-gate: sandbox code referencing ${STATE_DIR} is refused. ${HOWTO}`);
}
if (mentions(command)) {
  const readOnly = READ_ONLY_RE.test(command) && !WRITE_VERB_RE.test(command);
  if (!readOnly) {
    deny(
      `notif-registry-gate: this command names ${STATE_DIR} and is not a plain read. ${HOWTO} ` +
      "Reading it is fine: `cat`, `grep`, `jq`, or `notif-approve pending` with no write verb.",
    );
  }
}
