{
  "component_id": "src_hook.rs",
  "name": "Hook I/O Handler",
  "description": "Reads Claude Code PreToolUse JSON from stdin, evaluates tool calls against compiled policies and preflight constraints, and returns permission decisions (allow/deny/ask) to stdout. Implements multi-pass evaluation: checks for global disable/pause, enforces locked self-protection rules, evaluates hard policy rules with ensure script execution, checks soft preflight constraints, and logs violations with escalation tracking.",
  "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": "Tool parameters as JSON value (aliased as tool_input)",
          "validators": []
        }
      ],
      "item_type": "",
      "variants": [],
      "inner_types": [],
      "description": "Deserialized JSON input from Claude Code PreToolUse hook",
      "owner_component": ""
    },
    {
      "name": "HookResponse",
      "kind": "struct",
      "fields": [
        {
          "name": "hook_specific_output",
          "type_ref": "HookOutput",
          "required": true,
          "default": "",
          "description": "Nested output structure containing decision",
          "validators": []
        }
      ],
      "item_type": "",
      "variants": [],
      "inner_types": [],
      "description": "Wrapper for hook output expected by Claude Code",
      "owner_component": ""
    },
    {
      "name": "HookOutput",
      "kind": "struct",
      "fields": [
        {
          "name": "hook_event_name",
          "type_ref": "String",
          "required": true,
          "default": "",
          "description": "Event type, always 'PreToolUse'",
          "validators": []
        },
        {
          "name": "permission_decision",
          "type_ref": "String",
          "required": true,
          "default": "",
          "description": "Decision: 'allow', 'deny', or 'ask'",
          "validators": []
        },
        {
          "name": "reason",
          "type_ref": "Option<String>",
          "required": false,
          "default": "",
          "description": "Human-readable reason for deny/ask decisions",
          "validators": []
        },
        {
          "name": "additional_context",
          "type_ref": "Option<String>",
          "required": false,
          "default": "",
          "description": "Extended context for Claude (e.g., escalation warnings, alternative approaches)",
          "validators": []
        }
      ],
      "item_type": "",
      "variants": [],
      "inner_types": [],
      "description": "Actual hook response payload with permission decision",
      "owner_component": ""
    }
  ],
  "functions": [
    {
      "name": "evaluate_preflight_constraint",
      "description": "Evaluates a tool call against preflight soft constraints. Iterates through constraints, matches tool name against regex pattern, evaluates all conditions (AND logic), and returns the first matching constraint with formatted reason.",
      "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 configuration with soft constraints",
          "validators": []
        },
        {
          "name": "vault",
          "type_ref": "&Vault",
          "required": true,
          "default": "",
          "description": "Vault for condition evaluation context",
          "validators": []
        }
      ],
      "output_type": "Option<(SoftConstraint, String)>",
      "error_cases": [
        {
          "name": "InvalidRegex",
          "condition": "regex::Regex::new fails for constraint.tool_pattern",
          "error_type": "Silent skip (continue loop)",
          "error_data": {}
        }
      ],
      "preconditions": [],
      "postconditions": [
        "If Some, returns first constraint where tool_pattern matches call.tool_name AND all conditions evaluate to true",
        "Reason string formatted as '{constraint.reason} Instead: {constraint.alternative}'"
      ],
      "idempotent": false,
      "is_async": false,
      "side_effects": [
        "Calls policy::evaluate_condition for each constraint condition"
      ],
      "structured_side_effects": [],
      "performance_budget": null
    },
    {
      "name": "run_hook",
      "description": "Main hook entry point. Reads stdin JSON, parses as HookInput, performs multi-pass evaluation (disable check → pause check → hard policy → ensure resolution → preflight constraints), logs to vault, and emits decision to stdout. Returns 0 on success.",
      "inputs": [
        {
          "name": "policy",
          "type_ref": "&CompiledPolicy",
          "required": true,
          "default": "",
          "description": "Compiled hard policy rules",
          "validators": []
        },
        {
          "name": "vault",
          "type_ref": "Option<&Vault>",
          "required": true,
          "default": "",
          "description": "Optional vault for logging and preflight state",
          "validators": []
        }
      ],
      "output_type": "i32",
      "error_cases": [
        {
          "name": "StdinReadFailure",
          "condition": "io::stdin().read_to_string fails",
          "error_type": "emit_deny('Failed to read stdin'), return 0",
          "error_data": {}
        },
        {
          "name": "MalformedJSON",
          "condition": "serde_json::from_str fails to parse HookInput",
          "error_type": "emit_deny('Malformed hook input'), return 0",
          "error_data": {}
        }
      ],
      "preconditions": [],
      "postconditions": [
        "Always returns 0",
        "Emits exactly one HookResponse JSON to stdout",
        "If vault is Some, logs action with final decision and extracted amount/category"
      ],
      "idempotent": false,
      "is_async": false,
      "side_effects": [
        "Reads from stdin until EOF",
        "Writes JSON to stdout",
        "Calls crate::vault::is_disabled_file()",
        "Calls crate::vault::is_paused_file()",
        "Calls policy::evaluate()",
        "Calls resolve_ensure_result() for Ensure decisions",
        "Calls evaluate_preflight_constraint() for active preflights",
        "Calls vault.active_preflight(), vault.preflight_violations(), vault.log_preflight_violation(), vault.log_action()"
      ],
      "structured_side_effects": [
        {
          "kind": "reads_file",
          "target": "stdin",
          "description": ""
        },
        {
          "kind": "writes_file",
          "target": "stdout",
          "description": ""
        },
        {
          "kind": "mutates_state",
          "target": "vault (logs violations and actions)",
          "description": "Logs preflight violations and final action to vault if available"
        }
      ],
      "performance_budget": null
    },
    {
      "name": "emit_decision",
      "description": "Constructs HookResponse with decision, reason, and additional_context, serializes to JSON, and prints to stdout.",
      "inputs": [
        {
          "name": "decision",
          "type_ref": "&str",
          "required": true,
          "default": "",
          "description": "Permission decision: 'allow', 'deny', or 'ask'",
          "validators": []
        },
        {
          "name": "reason",
          "type_ref": "Option<String>",
          "required": true,
          "default": "",
          "description": "Optional reason for deny/ask",
          "validators": []
        },
        {
          "name": "additional_context",
          "type_ref": "Option<String>",
          "required": true,
          "default": "",
          "description": "Optional extended context for Claude",
          "validators": []
        }
      ],
      "output_type": "unit",
      "error_cases": [
        {
          "name": "SerializationFailure",
          "condition": "serde_json::to_string fails (should never happen with this structure)",
          "error_type": "Panic from unwrap()",
          "error_data": {}
        }
      ],
      "preconditions": [],
      "postconditions": [
        "Prints one line of JSON to stdout",
        "JSON contains hookSpecificOutput.hookEventName = 'PreToolUse'",
        "JSON contains hookSpecificOutput.permissionDecision = decision"
      ],
      "idempotent": false,
      "is_async": false,
      "side_effects": [
        "Writes to stdout"
      ],
      "structured_side_effects": [
        {
          "kind": "writes_file",
          "target": "stdout",
          "description": ""
        }
      ],
      "performance_budget": null
    },
    {
      "name": "emit_allow",
      "description": "Convenience function to emit allow decision with no reason or context.",
      "inputs": [],
      "output_type": "unit",
      "error_cases": [],
      "preconditions": [],
      "postconditions": [
        "Calls emit_decision('allow', None, None)"
      ],
      "idempotent": false,
      "is_async": false,
      "side_effects": [
        "Calls emit_decision"
      ],
      "structured_side_effects": [
        {
          "kind": "writes_file",
          "target": "stdout",
          "description": ""
        }
      ],
      "performance_budget": null
    },
    {
      "name": "emit_deny",
      "description": "Convenience function to emit deny decision with provided reason.",
      "inputs": [
        {
          "name": "reason",
          "type_ref": "&str",
          "required": true,
          "default": "",
          "description": "Denial reason",
          "validators": []
        }
      ],
      "output_type": "unit",
      "error_cases": [],
      "preconditions": [],
      "postconditions": [
        "Calls emit_decision('deny', Some(reason), None)"
      ],
      "idempotent": false,
      "is_async": false,
      "side_effects": [
        "Calls emit_decision"
      ],
      "structured_side_effects": [
        {
          "kind": "writes_file",
          "target": "stdout",
          "description": ""
        }
      ],
      "performance_budget": null
    },
    {
      "name": "resolve_ensure",
      "description": "Executes an ensure check script with timeout, captures stderr, and returns (success_bool, stderr_output). Resolves script path, spawns process with inherited stdin and piped stderr, waits with timeout, and kills on timeout.",
      "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": "ScriptNotFound",
          "condition": "Resolved script path does not exist",
          "error_type": "(false, 'Check script not found: {path}')",
          "error_data": {}
        },
        {
          "name": "ResolvePathFailure",
          "condition": "policy::resolve_ensure_script_path returns Err",
          "error_type": "(false, error_message)",
          "error_data": {}
        },
        {
          "name": "SpawnFailure",
          "condition": "Command::spawn fails",
          "error_type": "(false, 'Failed to spawn check script: {e}')",
          "error_data": {}
        },
        {
          "name": "Timeout",
          "condition": "Script does not complete within timeout",
          "error_type": "(false, 'Check script timed out after {timeout_secs}s')",
          "error_data": {}
        },
        {
          "name": "WaitError",
          "condition": "child.wait_timeout returns Err",
          "error_type": "(false, 'Error waiting for check script: {e}')",
          "error_data": {}
        }
      ],
      "preconditions": [],
      "postconditions": [
        "First element is true if script exited with status 0, false otherwise",
        "Second element contains stderr (up to 500 bytes) or error message",
        "Timeout clamped to [1, 30] seconds",
        "Process killed if timeout exceeded"
      ],
      "idempotent": false,
      "is_async": false,
      "side_effects": [
        "Spawns child process",
        "Inherits stdin",
        "Reads stderr from child",
        "Kills child on timeout"
      ],
      "structured_side_effects": [
        {
          "kind": "network_call",
          "target": "Spawns external process (check script)",
          "description": "Executes arbitrary script specified in config.check"
        }
      ],
      "performance_budget": null
    },
    {
      "name": "resolve_ensure_result",
      "description": "Converts an EvaluationResult with Ensure decision into Allow or Deny by running the ensure check script. If passed, returns Allow; if failed, returns Deny with formatted reason including 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": "MissingEnsureConfig",
          "condition": "result.ensure_config is None",
          "error_type": "Returns Deny with reason 'Ensure rule missing ensure config'",
          "error_data": {}
        }
      ],
      "preconditions": [],
      "postconditions": [
        "If ensure_config is Some and check passes: returns Allow with ensure_config cleared",
        "If ensure_config is Some and check fails: returns Deny with formatted reason, ensure_config cleared",
        "If ensure_config is None: returns Deny with 'Ensure rule missing ensure config'",
        "All other fields from input result preserved"
      ],
      "idempotent": false,
      "is_async": false,
      "side_effects": [
        "Calls resolve_ensure() which spawns external process"
      ],
      "structured_side_effects": [
        {
          "kind": "network_call",
          "target": "External ensure check script",
          "description": "Executes 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",
      "active_preflight",
      "preflight_violations",
      "log_preflight_violation",
      "log_action",
      "Vault",
      "Preflight",
      "PreflightViolation",
      "SoftConstraint"
    ]
  },
  "invariants": [
    "Hook event name is always 'PreToolUse'",
    "run_hook always returns 0 regardless of decision outcome",
    "Exactly one JSON response written to stdout per run_hook invocation",
    "Evaluation order: disable → pause (locked only) → hard policy → ensure → preflight constraints",
    "Locked rules enforced even during pause state",
    "Preflight violation count capped at 20 for display",
    "Stderr from ensure scripts truncated to 500 bytes",
    "Ensure script timeout clamped to [1, 30] seconds",
    "Parameters detail in vault logs truncated to 500 bytes",
    "Parameters summary in preflight violations truncated to 200 bytes",
    "Task summary in escalation messages truncated to 80 characters (77 + '...')",
    "amount defaults to 0.0 if not parseable or decision is not Allow",
    "category defaults to empty string if not present in parameters"
  ],
  "requires": [],
  "processing_register": "",
  "data_access": {
    "reads": [
      "stdin",
      "filesystem (via is_disabled_file, is_paused_file, resolve_ensure_script_path)"
    ],
    "writes": [
      "stdout",
      "vault (via log_action, log_preflight_violation)"
    ],
    "rationale": "Reads hook input from stdin, checks filesystem for disable/pause state, writes decision to stdout, and logs to vault database.",
    "side_effects": []
  },
  "authority": {
    "domains": [],
    "rationale": null
  }
}