Package com.tnsai.hooks.external
Class ExternalScriptHook
java.lang.Object
com.tnsai.hooks.external.ExternalScriptHook
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.Blockwith the script'sreasonandseverity"modify"— not yet supported in this foundation slice. Reconstructing a record-basedHookEventfrom 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 treatsmodifyasallow.
Exit codes
0— expect valid JSON on stdout2— intentional block (equivalent to"decision": "block")- anything else — error; apply
ScriptFailurePolicy
Environment
The script receives the configuredenvironment
variables plus three framework-injected ones:
TNSAI_HOOK_EVENT_TYPETNSAI_HOOK_EVENT_IDTNSAI_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
executablePathit was handed. A YAML loader / trust-root validator lands in a follow-up.
- Since:
- 0.5.0 (issue #64)
-
Nested Class Summary
Nested Classes -
Method Summary
Modifier and TypeMethodDescriptionInspect the event and return a decision.static ExternalScriptHook.Builderbuilder()The event class this hook observes.id()Short stable identifier for logs / audit trail.matcher()Further filter which instances ofHook.eventType()this hook cares about.name()intpriority()Priority in the chain.timeout()
-
Method Details
-
builder
-
eventType
Description copied from interface:HookThe event class this hook observes. Must be a concrete subtype ofHookEvent; the dispatcher usesClass.isAssignableFromto decide whether an event-in-flight matches this hook, so subclasses are matched too. -
matcher
Description copied from interface:HookFurther filter which instances ofHook.eventType()this hook cares about. Default: match all. -
priority
public int priority()Description copied from interface:HookPriority in the chain. Lower = earlier. Negative priorities run before any zero-priority hook — useful for "must run first" rules like security checks. Default0. -
id
Description copied from interface:HookShort stable identifier for logs / audit trail. Default: class name. Implementations may override when multiple hook instances of the same class need to be distinguished. -
name
-
executablePath
-
timeout
-
onFailure
-
apply
Description copied from interface:HookInspect the event and return a decision.
-