{
  "component_id": "src_hook.rs",
  "name": "Hook I/O – Claude Code PreToolUse Decision Engine",
  "description": "Reads Claude Code PreToolUse JSON from stdin, evaluates tool calls against compiled policies and preflight soft constraints, executes ensure check scripts, logs violations to vault, and returns permission decision (allow/deny/ask) on stdout. Implements pause mode (locked rules only) and disable mode (bypass all checks). Escalates preflight when violations exceed threshold.",
  "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<Value>",
          "required": false,
          "default": "",
          "description": "Tool parameters as JSON value, aliased as tool_input",
          "validators": []
        }
      ],
      "item_type": "",
      "variants": [],
      "inner_types": [],
      "description": "Deserialized JSON from stdin representing a pending tool call",
      "owner_component": ""
    },
    {
      "name": "HookResponse",
      "kind": "struct",
      "fields": [
        {
          "name": "hook_specific_output",
          "type_ref": "HookOutput",
          "required": true,
          "default": "",
          "description": "Nested output payload with decision details",
          "validators": []
        }
      ],
      "item_type": "",
      "variants": [],
      "inner_types": [],
      "description": "Claude Code expects hook responses wrapped in hookSpecificOutput",
      "owner_component": ""
    },
    {
      "name": "HookOutput",
      "kind": "struct",
      "fields": [
        {
          "name": "hook_event_name",
          "type_ref": "String",
          "required": true,
          "default": "",
          "description": "Always 'PreToolUse' for this hook",
          "validators": []
        },
        {
          "name": "permission_decision",
          "type_ref": "String",
          "required": true,
          "default": "",
          "description": "Decision string: '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": "Rich context for Claude, e.g., escalation details or violated constraint alternatives",
          "validators": []
        }
      ],
      "item_type": "",
      "variants": [],
      "inner_types": [],
      "description": "Inner hook response structure with permission decision and context",
      "owner_component": ""
    }
  ],
  "functions": [
    {
      "name": "evaluate_preflight_constraint",
      "description": "Evaluate a tool call against preflight soft constraints. Returns the first matching constraint (if any) by checking tool_pattern regex and all conditions via AND logic. Formats reason as '{constraint.reason} Instead: {constraint.alternative}'.",
      "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 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": "invalid_regex",
          "condition": "constraint.tool_pattern is not valid regex",
          "error_type": "Silent skip – continues to next constraint",
          "error_data": {}
        },
        {
          "name": "condition_eval_error",
          "condition": "policy::evaluate_condition returns Err or false",
          "error_type": "Sets all_match = false, breaks inner loop",
          "error_data": {}
        }
      ],
      "preconditions": [
        "preflight.constraints is iterable"
      ],
      "postconditions": [
        "Returns Some((constraint, reason)) if tool_pattern matches call.tool_name AND all conditions evaluate to true",
        "Returns None if no constraints match or all fail"
      ],
      "idempotent": false,
      "is_async": false,
      "side_effects": [
        "Calls policy::evaluate_condition for each constraint condition"
      ],
      "structured_side_effects": [
        {
          "kind": "mutates_state",
          "target": "None – pure evaluation",
          "description": ""
        }
      ],
      "performance_budget": null
    },
    {
      "name": "run_hook",
      "description": "Main hook entry point. Reads stdin, deserializes tool call, evaluates against policy and preflight constraints, resolves ensure checks, logs to vault, emits decision on stdout. Returns 0 always. Implements pause mode (locked rules only) and disable mode (bypass all checks). Escalates preflight if violations exceed threshold.",
      "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 state",
          "validators": []
        }
      ],
      "output_type": "i32",
      "error_cases": [
        {
          "name": "stdin_read_failure",
          "condition": "io::stdin().read_to_string fails",
          "error_type": "Emits deny with 'Failed to read stdin', returns 0",
          "error_data": {}
        },
        {
          "name": "malformed_json",
          "condition": "serde_json::from_str fails on input",
          "error_type": "Emits deny with 'Malformed hook input', returns 0",
          "error_data": {}
        }
      ],
      "preconditions": [],
      "postconditions": [
        "Always returns 0",
        "Emits exactly one JSON response to stdout",
        "If vault is Some, logs action to vault with category, amount, and detail",
        "If preflight violation occurs, logs PreflightViolation to vault"
      ],
      "idempotent": false,
      "is_async": false,
      "side_effects": [
        "Reads from stdin until EOF",
        "Writes JSON to stdout via println",
        "Calls crate::vault::is_disabled_file()",
        "Calls crate::vault::is_paused_file()",
        "Calls policy::evaluate",
        "Calls resolve_ensure_result if decision is Ensure",
        "Calls v.active_preflight() if vault is Some",
        "Calls v.preflight_violations if escalated",
        "Calls v.log_preflight_violation if soft constraint violated",
        "Calls v.log_action for final decision"
      ],
      "structured_side_effects": [
        {
          "kind": "reads_file",
          "target": "stdin",
          "description": "Reads entire stdin to string"
        },
        {
          "kind": "writes_file",
          "target": "stdout",
          "description": "Emits JSON decision response"
        },
        {
          "kind": "mutates_state",
          "target": "vault",
          "description": "Logs action and potentially preflight violation"
        },
        {
          "kind": "network_call",
          "target": "None – file I/O only",
          "description": ""
        }
      ],
      "performance_budget": null
    },
    {
      "name": "emit_decision",
      "description": "Serialize and print HookResponse with given decision, reason, and additional_context to stdout as JSON. Always sets hook_event_name to 'PreToolUse'.",
      "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 decision",
          "validators": []
        },
        {
          "name": "additional_context",
          "type_ref": "Option<String>",
          "required": false,
          "default": "",
          "description": "Optional rich context for Claude",
          "validators": []
        }
      ],
      "output_type": "()",
      "error_cases": [
        {
          "name": "serialization_failure",
          "condition": "serde_json::to_string fails (should never happen with these types)",
          "error_type": "Panic via .unwrap()",
          "error_data": {}
        }
      ],
      "preconditions": [],
      "postconditions": [
        "Prints valid JSON to stdout",
        "JSON contains hookSpecificOutput with hookEventName='PreToolUse'"
      ],
      "idempotent": false,
      "is_async": false,
      "side_effects": [
        "Prints JSON to stdout"
      ],
      "structured_side_effects": [
        {
          "kind": "writes_file",
          "target": "stdout",
          "description": "Prints JSON response"
        }
      ],
      "performance_budget": null
    },
    {
      "name": "emit_allow",
      "description": "Convenience wrapper that calls emit_decision with 'allow' and no reason or context.",
      "inputs": [],
      "output_type": "()",
      "error_cases": [],
      "preconditions": [],
      "postconditions": [
        "Emits allow decision to stdout"
      ],
      "idempotent": false,
      "is_async": false,
      "side_effects": [
        "Calls emit_decision('allow', None, None)"
      ],
      "structured_side_effects": [
        {
          "kind": "writes_file",
          "target": "stdout",
          "description": ""
        }
      ],
      "performance_budget": null
    },
    {
      "name": "emit_deny",
      "description": "Convenience wrapper that calls emit_decision with 'deny' and given reason.",
      "inputs": [
        {
          "name": "reason",
          "type_ref": "&str",
          "required": true,
          "default": "",
          "description": "Reason for denial",
          "validators": []
        }
      ],
      "output_type": "()",
      "error_cases": [],
      "preconditions": [],
      "postconditions": [
        "Emits deny decision with reason to stdout"
      ],
      "idempotent": false,
      "is_async": false,
      "side_effects": [
        "Calls emit_decision('deny', Some(reason), None)"
      ],
      "structured_side_effects": [
        {
          "kind": "writes_file",
          "target": "stdout",
          "description": ""
        }
      ],
      "performance_budget": null
    },
    {
      "name": "resolve_ensure",
      "description": "Run an ensure check script and return (passed, stderr_output). Resolves script path, spawns process with inherited stdin, null stdout, piped stderr, and waits with timeout (1-30s clamped). Returns (false, error_message) on any 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_failure",
          "condition": "policy::resolve_ensure_script_path returns Err",
          "error_type": "Returns (false, error_string)",
          "error_data": {}
        },
        {
          "name": "script_not_found",
          "condition": "!script_path.exists()",
          "error_type": "Returns (false, 'Check script not found: {path}')",
          "error_data": {}
        },
        {
          "name": "spawn_failure",
          "condition": "Command::spawn fails",
          "error_type": "Returns (false, 'Failed to spawn check script: {e}')",
          "error_data": {}
        },
        {
          "name": "timeout",
          "condition": "Script does not complete within timeout_secs",
          "error_type": "Kills child, returns (false, 'Check script timed out after {timeout_secs}s')",
          "error_data": {}
        },
        {
          "name": "wait_error",
          "condition": "child.wait_timeout returns Err",
          "error_type": "Returns (false, 'Error waiting for check script: {e}')",
          "error_data": {}
        }
      ],
      "preconditions": [
        "config.timeout is u32",
        "config.check is a path string"
      ],
      "postconditions": [
        "Returns (true, empty_string) if script exits 0 within timeout",
        "Returns (false, stderr_truncated_to_500) if script exits non-zero",
        "Returns (false, error_message) on spawn failure, timeout, or script not found",
        "Timeout is clamped to [1, 30] seconds"
      ],
      "idempotent": false,
      "is_async": false,
      "side_effects": [
        "Spawns child process",
        "Inherits stdin",
        "Reads stderr",
        "Kills child on timeout"
      ],
      "structured_side_effects": [
        {
          "kind": "mutates_state",
          "target": "child process",
          "description": "Spawns and potentially kills subprocess"
        },
        {
          "kind": "reads_file",
          "target": "stderr of child",
          "description": "Reads stderr, truncates to 500 bytes"
        }
      ],
      "performance_budget": {
        "p95_latency_ms": 30000,
        "max_memory_mb": null,
        "complexity": "O(1) – single process spawn"
      }
    },
    {
      "name": "resolve_ensure_result",
      "description": "Resolve an Ensure evaluation result by running the check script via resolve_ensure. Converts Ensure decision to Allow (if passed) or Deny (if failed). Returns modified EvaluationResult with decision updated and ensure_config cleared.",
      "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": "Returns Deny with reason='Ensure rule missing ensure config'",
          "error_data": {}
        },
        {
          "name": "check_script_failure",
          "condition": "resolve_ensure returns (false, stderr)",
          "error_type": "Returns Deny with reason='{message} -- {stderr}' or just message if stderr empty",
          "error_data": {}
        }
      ],
      "preconditions": [
        "result.decision should be Decision::Ensure (but function handles other cases)"
      ],
      "postconditions": [
        "If ensure_config is Some and check passes, returns result with decision=Allow and ensure_config=None",
        "If ensure_config is Some and check fails, returns result with decision=Deny, reason=message or default, ensure_config=None",
        "If ensure_config is None, returns result with decision=Deny, reason='Ensure rule missing ensure config'",
        "ensure_config is always cleared in returned result"
      ],
      "idempotent": false,
      "is_async": false,
      "side_effects": [
        "Calls resolve_ensure, which spawns subprocess"
      ],
      "structured_side_effects": [
        {
          "kind": "mutates_state",
          "target": "child process via resolve_ensure",
          "description": ""
        }
      ],
      "performance_budget": null
    }
  ],
  "dependencies": [
    "serde",
    "serde_json",
    "std::io",
    "std::process",
    "std::time",
    "wait_timeout",
    "regex",
    "crate::policy",
    "crate::vault"
  ],
  "dependency_contracts": {
    "crate::policy": [
      "CompiledPolicy",
      "Decision",
      "EnsureConfig",
      "EvaluationResult",
      "ToolCall",
      "evaluate",
      "evaluate_condition",
      "resolve_ensure_script_path"
    ],
    "crate::vault": [
      "Preflight",
      "PreflightViolation",
      "SoftConstraint",
      "Vault",
      "is_disabled_file",
      "is_paused_file"
    ]
  },
  "invariants": [
    "hook_event_name is always 'PreToolUse' in emitted responses",
    "run_hook always returns 0 regardless of decision",
    "Exactly one JSON response is emitted to stdout per run_hook invocation",
    "Timeout for ensure check scripts is clamped to [1, 30] seconds",
    "Stderr from ensure scripts is truncated to 500 bytes",
    "Parameter summaries in preflight violations are truncated to 200 chars",
    "Action detail logs are truncated to 500 chars",
    "Violation list in escalation context is limited to first 20 violations",
    "Parameter summary in violation context is truncated to 60 chars",
    "Task summary in escalation reason is truncated to 80 chars (77 + '...')",
    "If disabled file exists, all checks are bypassed and allow is emitted",
    "If paused file exists, only locked rules are enforced",
    "Hard deny always wins over soft constraints",
    "Preflight constraints are only evaluated if decision is not Deny after Pass 1",
    "Amount is only logged if final_decision == Allow",
    "Constraint conditions are evaluated with AND logic (all must match)"
  ],
  "requires": [],
  "processing_register": "",
  "data_access": {
    "reads": [
      "stdin",
      "filesystem (via is_disabled_file, is_paused_file, ensure script path check)"
    ],
    "writes": [
      "stdout",
      "vault (action log, preflight violation log)"
    ],
    "rationale": "Hook reads tool call JSON from stdin, checks filesystem for pause/disable state and ensure scripts, logs decisions and violations to vault, writes decision JSON to stdout",
    "side_effects": [
      {
        "type": "subprocess_spawn",
        "classification": "ensure check script execution",
        "fields": [
          "EnsureConfig.check",
          "timeout"
        ],
        "rationale": "Spawns external scripts defined in EnsureConfig to validate conditions before allowing actions"
      },
      {
        "type": "filesystem_read",
        "classification": "control state checks",
        "fields": [],
        "rationale": "Reads disable/pause sentinel files to determine enforcement mode"
      },
      {
        "type": "database_write",
        "classification": "audit logging",
        "fields": [
          "tool_name",
          "decision",
          "category",
          "amount",
          "detail",
          "preflight_id",
          "constraint_name",
          "parameters_summary",
          "alternative",
          "timestamp"
        ],
        "rationale": "Logs all tool call decisions and preflight violations to vault for audit trail"
      }
    ]
  },
  "authority": {
    "domains": [
      "tool_call_authorization",
      "preflight_constraint_enforcement",
      "ensure_check_execution"
    ],
    "rationale": "This module is the authoritative decision engine for Claude Code PreToolUse hook. It owns the logic for evaluating tool calls against policies, executing ensure checks, enforcing preflight constraints, and emitting final permission decisions."
  }
}