{
  "component_id": "src_hook.rs",
  "name": "Hook I/O Decision Engine",
  "description": "Reads Claude Code PreToolUse JSON from stdin, evaluates tool calls against compiled policies and preflight constraints, executes ensure check scripts, logs decisions to vault, and emits permission decisions (allow/deny/ask) to stdout. Handles pause mode (locked rules only), disable bypass, and escalated preflight violations with rich context.",
  "version": 1,
  "types": [
    {
      "name": "HookInput",
      "kind": "struct",
      "fields": [
        {
          "name": "tool_name",
          "type_ref": "String",
          "required": true,
          "default": "",
          "description": "Name of the tool Claude wants to invoke",
          "validators": []
        },
        {
          "name": "parameters",
          "type_ref": "Option<Value>",
          "required": false,
          "default": "",
          "description": "JSON parameters for the tool call (alias: tool_input)",
          "validators": []
        }
      ],
      "item_type": "",
      "variants": [],
      "inner_types": [],
      "description": "Deserialized input from Claude Code PreToolUse hook on stdin",
      "owner_component": ""
    },
    {
      "name": "HookResponse",
      "kind": "struct",
      "fields": [
        {
          "name": "hook_specific_output",
          "type_ref": "HookOutput",
          "required": true,
          "default": "",
          "description": "Nested output payload (JSON key: hookSpecificOutput)",
          "validators": []
        }
      ],
      "item_type": "",
      "variants": [],
      "inner_types": [],
      "description": "Top-level hook response structure expected by Claude Code",
      "owner_component": ""
    },
    {
      "name": "HookOutput",
      "kind": "struct",
      "fields": [
        {
          "name": "hook_event_name",
          "type_ref": "String",
          "required": true,
          "default": "",
          "description": "Always 'PreToolUse' (JSON key: hookEventName)",
          "validators": []
        },
        {
          "name": "permission_decision",
          "type_ref": "String",
          "required": true,
          "default": "",
          "description": "allow, deny, or ask (JSON key: permissionDecision)",
          "validators": []
        },
        {
          "name": "reason",
          "type_ref": "Option<String>",
          "required": false,
          "default": "",
          "description": "Human-readable reason for deny/ask (JSON key: permissionDecisionReason)",
          "validators": []
        },
        {
          "name": "additional_context",
          "type_ref": "Option<String>",
          "required": false,
          "default": "",
          "description": "Extended context for Claude, e.g., escalation instructions (JSON key: additionalContext)",
          "validators": []
        }
      ],
      "item_type": "",
      "variants": [],
      "inner_types": [],
      "description": "Hook decision payload with event name, permission decision, reason, and optional context",
      "owner_component": ""
    }
  ],
  "functions": [
    {
      "name": "evaluate_preflight_constraint",
      "description": "Evaluates a tool call against all preflight soft constraints and returns the first matching constraint with its formatted reason, or None if no constraints match. Matches tool_pattern regex and evaluates all conditions with AND logic.",
      "inputs": [
        {
          "name": "call",
          "type_ref": "&ToolCall",
          "required": true,
          "default": "",
          "description": "Tool call to evaluate",
          "validators": []
        },
        {
          "name": "preflight",
          "type_ref": "&Preflight",
          "required": true,
          "default": "",
          "description": "Active preflight containing soft constraints",
          "validators": []
        },
        {
          "name": "vault",
          "type_ref": "&Vault",
          "required": true,
          "default": "",
          "description": "Vault instance for condition evaluation context",
          "validators": []
        }
      ],
      "output_type": "Option<(SoftConstraint, String)>",
      "error_cases": [],
      "preconditions": [
        "preflight.constraints is iterable",
        "constraint.tool_pattern is a valid regex string"
      ],
      "postconditions": [
        "Returns Some((constraint, reason)) where reason = '{constraint.reason} Instead: {constraint.alternative}' if any constraint matches",
        "Returns None if no constraints match tool_name pattern or all conditions fail",
        "Invalid regex patterns in constraints are silently skipped"
      ],
      "idempotent": false,
      "is_async": false,
      "side_effects": [
        "Calls policy::evaluate_condition for each constraint condition"
      ],
      "structured_side_effects": [
        {
          "kind": "mutates_state",
          "target": "regex::Regex",
          "description": "Compiles regex patterns during iteration"
        }
      ],
      "performance_budget": null
    },
    {
      "name": "run_hook",
      "description": "Main hook entry point: reads JSON from stdin, evaluates against policy and preflight constraints, runs ensure checks if needed, logs decisions to vault, emits permission decision to stdout. Implements pause mode (locked rules only) and full disable bypass. Returns exit code 0 always.",
      "inputs": [
        {
          "name": "policy",
          "type_ref": "&CompiledPolicy",
          "required": true,
          "default": "",
          "description": "Compiled hard policy rules",
          "validators": []
        },
        {
          "name": "vault",
          "type_ref": "Option<&Vault>",
          "required": false,
          "default": "",
          "description": "Optional vault for logging and preflight data",
          "validators": []
        }
      ],
      "output_type": "i32",
      "error_cases": [
        {
          "name": "stdin_read_failure",
          "condition": "io::stdin().read_to_string() fails",
          "error_type": "io::Error",
          "error_data": {
            "decision": "deny",
            "reason": "Failed to read stdin"
          }
        },
        {
          "name": "malformed_json",
          "condition": "serde_json::from_str fails to parse HookInput",
          "error_type": "serde_json::Error",
          "error_data": {
            "decision": "deny",
            "reason": "Malformed hook input"
          }
        }
      ],
      "preconditions": [
        "stdin is readable",
        "policy is valid CompiledPolicy",
        "vault (if Some) is accessible"
      ],
      "postconditions": [
        "Always returns 0",
        "Exactly one decision emitted to stdout (allow/deny/ask)",
        "If vault is Some and decision made, action logged to vault",
        "If disable file exists, emits allow and returns 0 immediately",
        "If paused and not locked rule, emits allow",
        "If paused and locked deny/ensure fails, emits deny",
        "Escalated preflight emits ask with rich context",
        "Preflight violation logged to vault if constraint matched"
      ],
      "idempotent": false,
      "is_async": false,
      "side_effects": [
        "Reads all stdin to string",
        "Writes JSON decision to stdout",
        "May log action to vault",
        "May log preflight violation to vault",
        "Calls resolve_ensure_result if decision is Ensure",
        "Calls crate::vault::is_disabled_file()",
        "Calls crate::vault::is_paused_file()",
        "Calls policy::evaluate()"
      ],
      "structured_side_effects": [
        {
          "kind": "reads_file",
          "target": "stdin",
          "description": "Reads full JSON input from stdin"
        },
        {
          "kind": "writes_file",
          "target": "stdout",
          "description": "Writes JSON decision to stdout"
        },
        {
          "kind": "mutates_state",
          "target": "vault",
          "description": "Logs action and optionally preflight violations"
        }
      ],
      "performance_budget": null
    },
    {
      "name": "emit_decision",
      "description": "Serializes and prints a HookResponse with the given decision, optional reason, and optional additional context to stdout as JSON.",
      "inputs": [
        {
          "name": "decision",
          "type_ref": "&str",
          "required": true,
          "default": "",
          "description": "allow, deny, or ask",
          "validators": []
        },
        {
          "name": "reason",
          "type_ref": "Option<String>",
          "required": false,
          "default": "",
          "description": "Reason for deny/ask decision",
          "validators": []
        },
        {
          "name": "additional_context",
          "type_ref": "Option<String>",
          "required": false,
          "default": "",
          "description": "Extended context for Claude",
          "validators": []
        }
      ],
      "output_type": "unit",
      "error_cases": [
        {
          "name": "serialization_panic",
          "condition": "serde_json::to_string fails (unlikely for HookResponse)",
          "error_type": "panic",
          "error_data": {
            "context": "unwrap() on to_string result"
          }
        }
      ],
      "preconditions": [],
      "postconditions": [
        "Valid JSON HookResponse printed to stdout with hook_event_name='PreToolUse'",
        "permission_decision set to decision parameter",
        "reason and additional_context included only if Some"
      ],
      "idempotent": false,
      "is_async": false,
      "side_effects": [
        "Writes JSON to stdout via println!"
      ],
      "structured_side_effects": [
        {
          "kind": "writes_file",
          "target": "stdout",
          "description": "Prints JSON HookResponse"
        }
      ],
      "performance_budget": null
    },
    {
      "name": "emit_allow",
      "description": "Convenience wrapper that emits an allow decision with no reason or context.",
      "inputs": [],
      "output_type": "unit",
      "error_cases": [],
      "preconditions": [],
      "postconditions": [
        "emit_decision called with ('allow', None, None)"
      ],
      "idempotent": false,
      "is_async": false,
      "side_effects": [
        "Calls emit_decision"
      ],
      "structured_side_effects": [
        {
          "kind": "writes_file",
          "target": "stdout",
          "description": "Indirectly writes allow decision to stdout"
        }
      ],
      "performance_budget": null
    },
    {
      "name": "emit_deny",
      "description": "Convenience wrapper that emits a deny decision with the given reason.",
      "inputs": [
        {
          "name": "reason",
          "type_ref": "&str",
          "required": true,
          "default": "",
          "description": "Reason for denial",
          "validators": []
        }
      ],
      "output_type": "unit",
      "error_cases": [],
      "preconditions": [],
      "postconditions": [
        "emit_decision called with ('deny', Some(reason.into()), None)"
      ],
      "idempotent": false,
      "is_async": false,
      "side_effects": [
        "Calls emit_decision"
      ],
      "structured_side_effects": [
        {
          "kind": "writes_file",
          "target": "stdout",
          "description": "Indirectly writes deny decision to stdout"
        }
      ],
      "performance_budget": null
    },
    {
      "name": "resolve_ensure",
      "description": "Executes an ensure check script as a subprocess with timeout and returns (passed: bool, stderr: String). Resolves script path, spawns process with inherited stdin and piped stderr, enforces timeout (1-30s), captures stderr on failure.",
      "inputs": [
        {
          "name": "config",
          "type_ref": "&EnsureConfig",
          "required": true,
          "default": "",
          "description": "Ensure configuration with check script path and timeout",
          "validators": []
        }
      ],
      "output_type": "(bool, String)",
      "error_cases": [
        {
          "name": "script_path_resolution_failed",
          "condition": "policy::resolve_ensure_script_path returns Err",
          "error_type": "String",
          "error_data": {
            "passed": "false",
            "stderr": "error message from resolve_ensure_script_path"
          }
        },
        {
          "name": "script_not_found",
          "condition": "Resolved script path does not exist",
          "error_type": "String",
          "error_data": {
            "passed": "false",
            "stderr": "Check script not found: {path}"
          }
        },
        {
          "name": "spawn_failed",
          "condition": "Command::spawn returns Err",
          "error_type": "io::Error",
          "error_data": {
            "passed": "false",
            "stderr": "Failed to spawn check script: {e}"
          }
        },
        {
          "name": "timeout_exceeded",
          "condition": "child.wait_timeout returns Ok(None)",
          "error_type": "timeout",
          "error_data": {
            "passed": "false",
            "stderr": "Check script timed out after {timeout_secs}s"
          }
        },
        {
          "name": "wait_error",
          "condition": "child.wait_timeout returns Err",
          "error_type": "io::Error",
          "error_data": {
            "passed": "false",
            "stderr": "Error waiting for check script: {e}"
          }
        }
      ],
      "preconditions": [
        "config.timeout is positive integer",
        "config.check is a script path string"
      ],
      "postconditions": [
        "Returns (true, empty_string) if script exits 0 within timeout",
        "Returns (false, stderr_output) if script exits non-zero within timeout (stderr truncated to 500 bytes)",
        "Returns (false, timeout_message) if script exceeds timeout",
        "Returns (false, error_message) if script path resolution fails, script not found, or spawn fails",
        "Timeout clamped to 1-30 seconds"
      ],
      "idempotent": false,
      "is_async": false,
      "side_effects": [
        "Spawns subprocess via Command::new",
        "Inherits stdin from parent",
        "Pipes stderr",
        "Kills child process if timeout exceeded",
        "Reads stderr on non-zero exit"
      ],
      "structured_side_effects": [
        {
          "kind": "mutates_state",
          "target": "subprocess",
          "description": "Spawns, waits, and potentially kills child process"
        },
        {
          "kind": "reads_file",
          "target": "stderr pipe",
          "description": "Reads stderr from child process"
        }
      ],
      "performance_budget": null
    },
    {
      "name": "resolve_ensure_result",
      "description": "Transforms an EvaluationResult with decision=Ensure by running its ensure check script. If check passes, returns Allow; if fails, returns Deny with combined error message from config and stderr.",
      "inputs": [
        {
          "name": "result",
          "type_ref": "EvaluationResult",
          "required": true,
          "default": "",
          "description": "Evaluation result with decision=Ensure and optional ensure_config",
          "validators": []
        }
      ],
      "output_type": "EvaluationResult",
      "error_cases": [
        {
          "name": "missing_ensure_config",
          "condition": "result.ensure_config is None",
          "error_type": "misconfiguration",
          "error_data": {
            "decision": "Deny",
            "reason": "Ensure rule missing ensure config"
          }
        },
        {
          "name": "check_script_failed",
          "condition": "resolve_ensure returns (false, stderr)",
          "error_type": "check failure",
          "error_data": {
            "decision": "Deny",
            "reason": "config.message or 'Ensure check {check} failed' -- {stderr}"
          }
        }
      ],
      "preconditions": [
        "result.decision == Decision::Ensure"
      ],
      "postconditions": [
        "If ensure_config is Some and check passes: returns EvaluationResult with decision=Allow, ensure_config=None, other fields from input",
        "If ensure_config is Some and check fails: returns EvaluationResult with decision=Deny, reason=Some(formatted message + stderr), ensure_config=None",
        "If ensure_config is None: returns EvaluationResult with decision=Deny, reason=Some('Ensure rule missing ensure config')",
        "Original result fields (matched_locked, etc.) preserved except decision, reason, ensure_config"
      ],
      "idempotent": false,
      "is_async": false,
      "side_effects": [
        "Calls resolve_ensure if ensure_config is Some",
        "Spawns subprocess via resolve_ensure"
      ],
      "structured_side_effects": [
        {
          "kind": "mutates_state",
          "target": "subprocess",
          "description": "Indirectly spawns check script via resolve_ensure"
        }
      ],
      "performance_budget": null
    }
  ],
  "dependencies": [
    "serde",
    "serde_json",
    "std::io",
    "std::process",
    "std::time",
    "wait_timeout",
    "regex",
    "crate::policy",
    "crate::vault"
  ],
  "dependency_contracts": {
    "crate::policy": [
      "evaluate",
      "evaluate_condition",
      "resolve_ensure_script_path",
      "CompiledPolicy",
      "Decision",
      "EnsureConfig",
      "EvaluationResult",
      "ToolCall"
    ],
    "crate::vault": [
      "is_disabled_file",
      "is_paused_file",
      "Vault::active_preflight",
      "Vault::preflight_violations",
      "Vault::log_preflight_violation",
      "Vault::log_action",
      "Preflight",
      "PreflightViolation",
      "SoftConstraint"
    ],
    "serde_json": [
      "from_str",
      "to_string",
      "Value"
    ],
    "regex": [
      "Regex::new",
      "Regex::is_match"
    ],
    "std::process": [
      "Command",
      "ChildExt::wait_timeout"
    ],
    "std::io": [
      "Read::read_to_string",
      "Read::read_to_end"
    ],
    "std::time": [
      "SystemTime::now",
      "Duration::from_secs",
      "UNIX_EPOCH"
    ]
  },
  "invariants": [
    "Always returns exit code 0 from run_hook",
    "Exactly one JSON decision emitted to stdout per run_hook invocation",
    "Disable file bypasses all policy enforcement",
    "Pause mode enforces only locked rules",
    "Escalated preflight always emits Decision::Ask",
    "Ensure timeout clamped to [1, 30] seconds",
    "Vault logging is best-effort (errors ignored)",
    "Stderr from check scripts truncated to 500 bytes",
    "Preflight violation parameters_summary truncated to 200 bytes",
    "Preflight violations limited to 20 when building escalation context"
  ],
  "requires": [],
  "processing_register": "",
  "data_access": {
    "reads": [
      "stdin",
      "filesystem (via is_disabled_file, is_paused_file, script execution)"
    ],
    "writes": [
      "stdout",
      "vault (action logs, preflight violations)"
    ],
    "rationale": "Reads hook input JSON from stdin, checks pause/disable files, executes check scripts from filesystem, writes decision JSON to stdout, logs to vault database",
    "side_effects": []
  },
  "authority": {
    "domains": [],
    "rationale": null
  }
}