Class ExternalScriptHook

java.lang.Object
com.tnsai.hooks.external.ExternalScriptHook
All Implemented Interfaces:
Hook<HookEvent>

public final class ExternalScriptHook extends Object implements Hook<HookEvent>
Invokes an external process as a hook (issue #64). Serializes the HookEvent to JSON on stdin, runs the script with a timeout, reads stdout, parses the decision, and maps it back to a HookResult.

Complements the Java-native Hook SPI from #58 — lets consumers write hooks in any language (bash, Python, Go, …) and reuse existing Claude Code / SRE runbook scripts without rewriting them in Java.

JSON protocol (hook_protocol_version 1)

Request (on stdin)


 {
   "tnsai_version": "0.5.0",
   "hook_protocol_version": "1",
   "event_type": "PreToolUse",
   "event_id": "...",
   "timestamp": "...",
   "context": { ... },
   "payload": { ... }       // event-specific fields minus (event_id, timestamp, context)
 }
 

Response (on stdout)


 {
   "hook_protocol_version": "1",
   "decision": "allow" | "skip" | "block" | "modify",
   "reason": "...",          // required for block
   "severity": "LOW" | ... | "CRITICAL",   // block only
   "modified_payload": {...}, // modify only — NOT YET SUPPORTED (see below)
   "metadata": {...},         // free-form, logged but not acted on
   "continue_chain": false    // optional; reserved for future use
 }
 

Decision mapping

  • "allow" / "skip" → HookResult.Allow
  • "block" → HookResult.Block with the script's reason and severity
  • "modify" — not yet supported in this foundation slice. Reconstructing a record-based HookEvent from a modified payload map requires component-level reflection and type coercion that's a focused follow-up. Today the hook logs a structured warning and treats modify as allow.

Exit codes

  • 0 — expect valid JSON on stdout
  • 2 — intentional block (equivalent to "decision": "block")
  • anything else — error; apply ScriptFailurePolicy

Environment

The script receives the configured environment variables plus three framework-injected ones:
  • TNSAI_HOOK_EVENT_TYPE
  • TNSAI_HOOK_EVENT_ID
  • TNSAI_HOOK_PROTOCOL_VERSION = "1"

Security

  • Event flows on stdin, never argv — prevents command injection if event content is attacker-influenced.
  • Timeout enforced via Process.destroy(); on hard deadline the process is destroyed, subsequent output discarded.
  • Path whitelist and permission checks are operator-side concerns; this class trusts the executablePath it was handed. A YAML loader / trust-root validator lands in a follow-up.
Since:
0.5.0 (issue #64)
  • Method Details

    • builder

      public static ExternalScriptHook.Builder builder()
    • eventType

      public Class<HookEvent> eventType()
      Description copied from interface: Hook
      The event class this hook observes. Must be a concrete subtype of HookEvent; the dispatcher uses Class.isAssignableFrom to decide whether an event-in-flight matches this hook, so subclasses are matched too.
      Specified by:
      eventType in interface Hook<HookEvent>
    • matcher

      public HookMatcher<HookEvent> matcher()
      Description copied from interface: Hook
      Further filter which instances of Hook.eventType() this hook cares about. Default: match all.
      Specified by:
      matcher in interface Hook<HookEvent>
    • priority

      public int priority()
      Description copied from interface: Hook
      Priority in the chain. Lower = earlier. Negative priorities run before any zero-priority hook — useful for "must run first" rules like security checks. Default 0.
      Specified by:
      priority in interface Hook<HookEvent>
    • id

      public String id()
      Description copied from interface: Hook
      Short stable identifier for logs / audit trail. Default: class name. Implementations may override when multiple hook instances of the same class need to be distinguished.
      Specified by:
      id in interface Hook<HookEvent>
    • name

      public String name()
    • executablePath

      public Path executablePath()
    • timeout

      public Duration timeout()
    • onFailure

      public ScriptFailurePolicy onFailure()
    • apply

      public HookResult<HookEvent> apply(HookEvent event)
      Description copied from interface: Hook
      Inspect the event and return a decision.
      Specified by:
      apply in interface Hook<HookEvent>