{
  "component_id": "src_hook.rs",
  "name": "Hook I/O - Policy Enforcement Decision Engine",
  "description": "Reads Claude Code PreToolUse JSON from stdin, evaluates tool calls against compiled policy and preflight constraints, executes ensure check scripts when needed, and returns allow/deny/ask decisions on stdout. Implements two-pass enforcement: hard policy rules followed by soft preflight constraints, with escalation context generation and violation logging.",
  "version": 1,
  "types": [
    {
      "name": "HookInput",
      "kind": "struct",
      "fields": [
        {
          "name": "tool_name",
          "type_ref": "String",
          "required": true,
          "default": "",
          "description": "Name of the tool being invoked",
          "validators": []
        },
        {
          "name": "parameters",
          "type_ref": "Option<serde_json::Value>",
          "required": false,
          "default": "",
          "description": "Tool parameters as JSON (aliased from 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 (serialized as hookSpecificOutput)",
          "validators": []
        }
      ],
      "item_type": "",
      "variants": [],
      "inner_types": [],
      "description": "Top-level response wrapper expected by Claude Code",
      "owner_component": ""
    },
    {
      "name": "HookOutput",
      "kind": "struct",
      "fields": [
        {
          "name": "hook_event_name",
          "type_ref": "String",
          "required": true,
          "default": "",
          "description": "Always 'PreToolUse'",
          "validators": []
        },
        {
          "name": "permission_decision",
          "type_ref": "String",
          "required": true,
          "default": "",
          "description": "One of: allow, deny, 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": "Rich context for Claude (e.g., escalation messages, violation details)",
          "validators": []
        }
      ],
      "item_type": "",
      "variants": [],
      "inner_types": [],
      "description": "Actual hook decision payload with reason and context",
      "owner_component": ""
    }
  ],
  "functions": [
    {
      "name": "evaluate_preflight_constraint",
      "description": "Evaluates a tool call against active preflight soft constraints. Returns the first matching constraint with constructed reason message, or None if no constraints match. Uses regex matching on tool name and AND logic for all conditions.",
      "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 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 contains valid constraints with tool_pattern and conditions",
        "vault is accessible for condition evaluation"
      ],
      "postconditions": [
        "Returns Some((constraint, reason)) for first matching constraint",
        "Reason string is formatted as '{constraint.reason} Instead: {constraint.alternative}'",
        "Returns None if no constraint matches tool_name or all conditions fail"
      ],
      "idempotent": false,
      "is_async": false,
      "side_effects": [
        "Compiles regex patterns from constraint.tool_pattern (invalid patterns are silently skipped)",
        "Calls policy::evaluate_condition for each constraint condition"
      ],
      "structured_side_effects": [
        {
          "kind": "mutates_state",
          "target": "regex compilation cache (implicit)",
          "description": "Regex::new may cache compiled patterns"
        }
      ],
      "performance_budget": null
    },
    {
      "name": "run_hook",
      "description": "Main hook entry point. Reads JSON from stdin, evaluates tool call through two-pass enforcement (hard policy + soft preflight), resolves ensure checks, logs violations and actions to vault, emits decision to stdout. Returns 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 constraint access",
          "validators": []
        }
      ],
      "output_type": "i32",
      "error_cases": [
        {
          "name": "stdin_read_failure",
          "condition": "io::stdin().read_to_string fails",
          "error_type": "recoverable",
          "error_data": {
            "decision": "deny",
            "reason": "Failed to read stdin"
          }
        },
        {
          "name": "json_parse_failure",
          "condition": "serde_json::from_str fails on input",
          "error_type": "recoverable",
          "error_data": {
            "decision": "deny",
            "reason": "Malformed hook input"
          }
        }
      ],
      "preconditions": [
        "stdin contains valid JSON matching HookInput schema or is readable",
        "policy is compiled and valid"
      ],
      "postconditions": [
        "Always returns 0",
        "Exactly one decision emitted to stdout as JSON HookResponse",
        "If vault present, action logged with decision, tool name, category, amount, and parameter detail (truncated to 500 chars)",
        "If disabled file exists, emits allow without evaluation",
        "If paused file exists, only enforces locked rules; non-locked calls are allowed",
        "Hard deny decisions short-circuit preflight evaluation",
        "Escalated preflights generate Decision::Ask with rich context detailing up to 20 violations",
        "Non-escalated preflight violations are logged to vault and decision determined by constraint action (DENY or ASK)"
      ],
      "idempotent": false,
      "is_async": false,
      "side_effects": [
        "Reads entire stdin to string",
        "Writes JSON decision to stdout via println!",
        "Calls crate::vault::is_disabled_file()",
        "Calls crate::vault::is_paused_file()",
        "Calls policy::evaluate()",
        "May call resolve_ensure_result() which spawns child processes",
        "May log preflight violations to vault via vault.log_preflight_violation()",
        "Logs action to vault via vault.log_action() if vault is Some",
        "Retrieves violation history via vault.preflight_violations()",
        "Retrieves active preflight via vault.active_preflight()"
      ],
      "structured_side_effects": [
        {
          "kind": "reads_file",
          "target": "disabled/paused marker files",
          "description": "Checks file existence via vault module"
        },
        {
          "kind": "writes_file",
          "target": "vault database",
          "description": "Logs violations and actions if vault is present"
        },
        {
          "kind": "network_call",
          "target": "stdout",
          "description": "Emits JSON decision to stdout"
        },
        {
          "kind": "mutates_state",
          "target": "child process execution",
          "description": "May spawn check scripts via resolve_ensure_result"
        }
      ],
      "performance_budget": null
    },
    {
      "name": "emit_decision",
      "description": "Serializes and prints a HookResponse JSON to stdout with specified decision, reason, and additional_context.",
      "inputs": [
        {
          "name": "decision",
          "type_ref": "&str",
          "required": true,
          "default": "",
          "description": "Permission decision: allow, deny, or ask",
          "validators": []
        },
        {
          "name": "reason",
          "type_ref": "Option<String>",
          "required": false,
          "default": "",
          "description": "Optional reason for the decision",
          "validators": []
        },
        {
          "name": "additional_context",
          "type_ref": "Option<String>",
          "required": false,
          "default": "",
          "description": "Optional additional context for Claude",
          "validators": []
        }
      ],
      "output_type": "unit",
      "error_cases": [
        {
          "name": "serialization_panic",
          "condition": "serde_json::to_string fails (should never happen with this schema)",
          "error_type": "panic",
          "error_data": {
            "message": "unwrap on serialization failure"
          }
        }
      ],
      "preconditions": [],
      "postconditions": [
        "JSON printed to stdout in HookResponse format with hook_event_name='PreToolUse'",
        "Serialization always succeeds (unwrap on serde_json::to_string)"
      ],
      "idempotent": false,
      "is_async": false,
      "side_effects": [
        "Writes to stdout"
      ],
      "structured_side_effects": [
        {
          "kind": "network_call",
          "target": "stdout",
          "description": "Prints JSON response"
        }
      ],
      "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": [
        "Calls emit_decision with decision='allow', reason=None, additional_context=None"
      ],
      "idempotent": false,
      "is_async": false,
      "side_effects": [
        "Calls emit_decision"
      ],
      "structured_side_effects": [
        {
          "kind": "network_call",
          "target": "stdout",
          "description": "Indirectly writes to stdout via emit_decision"
        }
      ],
      "performance_budget": null
    },
    {
      "name": "emit_deny",
      "description": "Convenience wrapper that emits a deny decision with a reason string and no additional context.",
      "inputs": [
        {
          "name": "reason",
          "type_ref": "&str",
          "required": true,
          "default": "",
          "description": "Reason for denial",
          "validators": []
        }
      ],
      "output_type": "unit",
      "error_cases": [],
      "preconditions": [],
      "postconditions": [
        "Calls emit_decision with decision='deny', reason=Some(reason.into()), additional_context=None"
      ],
      "idempotent": false,
      "is_async": false,
      "side_effects": [
        "Calls emit_decision"
      ],
      "structured_side_effects": [
        {
          "kind": "network_call",
          "target": "stdout",
          "description": "Indirectly writes to stdout via emit_decision"
        }
      ],
      "performance_budget": null
    },
    {
      "name": "resolve_ensure",
      "description": "Executes an ensure check script with timeout, returns (passed: bool, stderr: String). Script must exist and be executable. Timeout is clamped to 1-30 seconds. Exit code 0 means passed, non-zero or timeout means failed.",
      "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_not_found",
          "condition": "policy::resolve_ensure_script_path returns Err or path does not exist",
          "error_type": "recoverable",
          "error_data": {
            "passed": "false",
            "stderr": "Check script not found: {path}"
          }
        },
        {
          "name": "spawn_failure",
          "condition": "Command::spawn fails",
          "error_type": "recoverable",
          "error_data": {
            "passed": "false",
            "stderr": "Failed to spawn check script: {error}"
          }
        },
        {
          "name": "timeout",
          "condition": "Script does not complete within timeout_secs",
          "error_type": "recoverable",
          "error_data": {
            "passed": "false",
            "stderr": "Check script timed out after {timeout_secs}s"
          }
        },
        {
          "name": "wait_error",
          "condition": "wait_timeout returns Err",
          "error_type": "recoverable",
          "error_data": {
            "passed": "false",
            "stderr": "Error waiting for check script: {error}"
          }
        }
      ],
      "preconditions": [
        "config.check is a valid script path reference",
        "config.timeout is a positive integer"
      ],
      "postconditions": [
        "Returns (true, \"\") if script exits 0 within timeout",
        "Returns (false, stderr_text) if script exits non-zero, capturing up to 500 chars of stderr",
        "Returns (false, error_message) if script not found, spawn fails, or timeout exceeded",
        "Timeout is clamped to [1, 30] seconds",
        "Child process is killed if timeout occurs"
      ],
      "idempotent": true,
      "is_async": false,
      "side_effects": [
        "Spawns child process executing check script",
        "Inherits stdin, nulls stdout, pipes stderr",
        "Kills child process if timeout occurs",
        "Reads up to 500 bytes from stderr"
      ],
      "structured_side_effects": [
        {
          "kind": "mutates_state",
          "target": "child process",
          "description": "Spawns, waits, and potentially kills script process"
        },
        {
          "kind": "reads_file",
          "target": "check script path",
          "description": "Executes external script file"
        }
      ],
      "performance_budget": null
    },
    {
      "name": "resolve_ensure_result",
      "description": "Transforms an EvaluationResult with Decision::Ensure by running the ensure check script. Converts to Decision::Allow if check passes, Decision::Deny if check fails. Returns modified EvaluationResult.",
      "inputs": [
        {
          "name": "result",
          "type_ref": "EvaluationResult",
          "required": true,
          "default": "",
          "description": "Evaluation result with potential ensure_config",
          "validators": []
        }
      ],
      "output_type": "EvaluationResult",
      "error_cases": [],
      "preconditions": [
        "result is a valid EvaluationResult"
      ],
      "postconditions": [
        "If result.ensure_config is Some and check passes: returns result with decision=Allow, ensure_config=None",
        "If result.ensure_config is Some and check fails: returns result with decision=Deny, reason=config.message or default message + stderr, ensure_config=None",
        "If result.ensure_config is None: returns result with decision=Deny, reason='Ensure rule missing ensure config'",
        "Always clears ensure_config in returned result",
        "Preserves all other fields from input result (matched_locked, etc.)"
      ],
      "idempotent": false,
      "is_async": false,
      "side_effects": [
        "Calls resolve_ensure which spawns child process if ensure_config is present"
      ],
      "structured_side_effects": [
        {
          "kind": "mutates_state",
          "target": "child process",
          "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"
    ]
  },
  "invariants": [
    "run_hook always returns 0 regardless of decision outcome",
    "Exactly one JSON decision is emitted to stdout per run_hook invocation",
    "Disabled file bypasses all policy and preflight evaluation",
    "Paused mode only enforces locked rules; non-locked rules are allowed",
    "Hard deny decisions (from policy evaluation) short-circuit preflight constraint checks",
    "Escalated preflights always result in Decision::Ask with violation context",
    "Ensure timeouts are clamped to [1, 30] seconds",
    "Vault logging truncates parameter detail to 500 characters",
    "Violation parameters_summary is truncated to 200 characters",
    "Escalation context shows up to 20 violations grouped by constraint",
    "Tool parameter detail is truncated to 60 characters per violation in escalation context",
    "Amount is only logged if final_decision == Decision::Allow, otherwise 0.0",
    "hook_event_name is always 'PreToolUse' in emitted responses",
    "Child processes for ensure checks are killed if they exceed timeout"
  ],
  "requires": [],
  "processing_register": "Two-pass enforcement: (1) Hard policy evaluation via policy::evaluate with potential ensure resolution (spawns check script, converts to allow/deny). (2) If not hard-denied, check active preflight soft constraints; if escalated, build rich violation context and force Ask decision; if not escalated but constraint violated, log violation and apply constraint action (deny/ask). Always log final action to vault with tool name, decision, category, amount (if allowed), and parameter detail. Emit single JSON decision to stdout.",
  "data_access": {
    "reads": [
      "stdin",
      "filesystem (check script paths)",
      "filesystem (disabled/paused marker files)"
    ],
    "writes": [
      "stdout",
      "vault database (violations and actions)"
    ],
    "rationale": "Hook reads tool call JSON from stdin, evaluates against policy/vault, spawns check scripts for ensure rules, and writes decision JSON to stdout while logging enforcement events to vault.",
    "side_effects": [
      {
        "type": "process_spawn",
        "classification": "ensure check scripts",
        "fields": [],
        "rationale": "Executes user-defined check scripts to validate ensure conditions"
      },
      {
        "type": "database_write",
        "classification": "vault logging",
        "fields": [
          "preflight_id",
          "constraint_name",
          "tool_name",
          "parameters_summary",
          "alternative",
          "timestamp",
          "category",
          "amount",
          "detail"
        ],
        "rationale": "Persists preflight violations and tool action audit trail"
      }
    ]
  },
  "authority": {
    "domains": [
      "policy_enforcement_decisions",
      "preflight_constraint_violations",
      "ensure_check_execution"
    ],
    "rationale": "This module owns the decision logic for tool call approval/denial, violation detection and logging for soft constraints, and orchestration of external ensure check scripts."
  }
}