{
  "component_id": "src_hook.rs",
  "name": "Hook I/O - Claude Code PreToolUse Integration",
  "description": "Reads Claude Code PreToolUse JSON from stdin, evaluates tool calls against compiled policies and preflight soft constraints, executes ensure check scripts, logs decisions to vault, and returns permission decisions (allow/deny/ask) on stdout. Handles disabled mode bypass, pause mode (enforcing only locked rules), preflight escalation with rich context, and soft constraint violation 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 from tool_input",
          "validators": []
        }
      ],
      "item_type": "",
      "variants": [],
      "inner_types": [],
      "description": "Deserialized JSON input from Claude Code PreToolUse hook containing tool name and parameters",
      "owner_component": ""
    },
    {
      "name": "HookResponse",
      "kind": "struct",
      "fields": [
        {
          "name": "hook_specific_output",
          "type_ref": "HookOutput",
          "required": true,
          "default": "",
          "description": "Nested output structure",
          "validators": []
        }
      ],
      "item_type": "",
      "variants": [],
      "inner_types": [],
      "description": "Top-level hook response wrapper expected by Claude Code, contains 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": "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": "Extended context for Claude, especially for preflight escalations and violations",
          "validators": []
        }
      ],
      "item_type": "",
      "variants": [],
      "inner_types": [],
      "description": "Actual hook output with permission decision, reason, and additional context for Claude",
      "owner_component": ""
    }
  ],
  "functions": [
    {
      "name": "evaluate_preflight_constraint",
      "description": "Evaluates a tool call against preflight soft constraints. Returns the first matching constraint (if any) by checking tool_pattern regex and all AND-ed conditions.",
      "inputs": [
        {
          "name": "call",
          "type_ref": "&ToolCall",
          "required": true,
          "default": "",
          "description": "The tool call to evaluate",
          "validators": []
        },
        {
          "name": "preflight",
          "type_ref": "&Preflight",
          "required": true,
          "default": "",
          "description": "Active preflight with constraints to check",
          "validators": []
        },
        {
          "name": "vault",
          "type_ref": "&Vault",
          "required": true,
          "default": "",
          "description": "Vault context for condition evaluation",
          "validators": []
        }
      ],
      "output_type": "Option<(SoftConstraint, String)>",
      "error_cases": [
        {
          "name": "regex_compile_error",
          "condition": "constraint.tool_pattern is invalid regex",
          "error_type": "silent skip",
          "error_data": {
            "behavior": "continues to next constraint"
          }
        },
        {
          "name": "condition_evaluation_error",
          "condition": "policy::evaluate_condition returns Err or Ok(false)",
          "error_type": "silent skip",
          "error_data": {
            "behavior": "treats as non-match, breaks inner loop"
          }
        }
      ],
      "preconditions": [
        "preflight contains valid constraints",
        "call.tool_name is non-empty"
      ],
      "postconditions": [
        "If Some returned, constraint.tool_pattern matched call.tool_name and all conditions passed",
        "Reason string formatted as '{constraint.reason} Instead: {constraint.alternative}'"
      ],
      "idempotent": false,
      "is_async": false,
      "side_effects": [
        "Compiles regex for each constraint tool_pattern",
        "Calls policy::evaluate_condition for each condition"
      ],
      "structured_side_effects": [
        {
          "kind": "mutates_state",
          "target": "regex compilation (transient)",
          "description": ""
        }
      ],
      "performance_budget": null
    },
    {
      "name": "run_hook",
      "description": "Main entry point. Reads JSON from stdin, evaluates against policy and preflight constraints, resolves ensure checks, logs 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 checks",
          "validators": []
        }
      ],
      "output_type": "i32",
      "error_cases": [
        {
          "name": "stdin_read_failure",
          "condition": "io::stdin().read_to_string() returns Err",
          "error_type": "graceful",
          "error_data": {
            "response": "emit_deny('Failed to read stdin')",
            "exit_code": "0"
          }
        },
        {
          "name": "malformed_json",
          "condition": "serde_json::from_str fails to parse HookInput",
          "error_type": "graceful",
          "error_data": {
            "response": "emit_deny('Malformed hook input')",
            "exit_code": "0"
          }
        }
      ],
      "preconditions": [
        "stdin contains valid JSON matching HookInput schema",
        "policy is compiled and valid"
      ],
      "postconditions": [
        "Always returns 0",
        "Emits exactly one HookResponse JSON to stdout",
        "If vault present, logs action with decision, category, amount, detail",
        "If preflight violation occurred, logs PreflightViolation to vault"
      ],
      "idempotent": false,
      "is_async": false,
      "side_effects": [
        "Reads all stdin to string",
        "Writes JSON to stdout",
        "Checks file existence: crate::vault::is_disabled_file(), is_paused_file()",
        "May execute external check scripts via resolve_ensure_result",
        "Logs to vault if present"
      ],
      "structured_side_effects": [
        {
          "kind": "reads_file",
          "target": "stdin",
          "description": "Reads entire stdin stream"
        },
        {
          "kind": "writes_file",
          "target": "stdout",
          "description": "Writes JSON decision response"
        },
        {
          "kind": "reads_file",
          "target": "disable/pause marker files",
          "description": "Checks is_disabled_file() and is_paused_file()"
        },
        {
          "kind": "mutates_state",
          "target": "vault",
          "description": "Logs actions and preflight violations"
        }
      ],
      "performance_budget": null
    },
    {
      "name": "emit_decision",
      "description": "Serializes and prints HookResponse with given decision, reason, and additional context to stdout as JSON.",
      "inputs": [
        {
          "name": "decision",
          "type_ref": "&str",
          "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",
          "validators": []
        },
        {
          "name": "additional_context",
          "type_ref": "Option<String>",
          "required": false,
          "default": "",
          "description": "Extended context for Claude",
          "validators": []
        }
      ],
      "output_type": "()",
      "error_cases": [
        {
          "name": "serialization_panic",
          "condition": "serde_json::to_string panics (unlikely with well-formed HookResponse)",
          "error_type": "panic",
          "error_data": {
            "behavior": "unwrap() will panic"
          }
        }
      ],
      "preconditions": [],
      "postconditions": [
        "Prints exactly one JSON line to stdout",
        "JSON conforms to HookResponse schema"
      ],
      "idempotent": false,
      "is_async": false,
      "side_effects": [
        "Writes to stdout"
      ],
      "structured_side_effects": [
        {
          "kind": "writes_file",
          "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": "()",
      "error_cases": [],
      "preconditions": [],
      "postconditions": [
        "Calls emit_decision('allow', None, None)"
      ],
      "idempotent": false,
      "is_async": false,
      "side_effects": [
        "Writes to stdout via emit_decision"
      ],
      "structured_side_effects": [
        {
          "kind": "writes_file",
          "target": "stdout",
          "description": ""
        }
      ],
      "performance_budget": null
    },
    {
      "name": "emit_deny",
      "description": "Convenience wrapper that emits a deny decision with given reason and no additional context.",
      "inputs": [
        {
          "name": "reason",
          "type_ref": "&str",
          "required": true,
          "default": "",
          "description": "Reason for denial",
          "validators": []
        }
      ],
      "output_type": "()",
      "error_cases": [],
      "preconditions": [],
      "postconditions": [
        "Calls emit_decision('deny', Some(reason.into()), None)"
      ],
      "idempotent": false,
      "is_async": false,
      "side_effects": [
        "Writes to stdout via 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, returns (passed: bool, stderr_output: String). Spawns process, waits up to config.timeout (clamped 1-30s), 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": "script_path_resolution_error",
          "condition": "policy::resolve_ensure_script_path returns Err",
          "error_type": "graceful",
          "error_data": {
            "return": "(false, error_message)"
          }
        },
        {
          "name": "script_not_found",
          "condition": "!script_path.exists()",
          "error_type": "graceful",
          "error_data": {
            "return": "(false, 'Check script not found: {path}')"
          }
        },
        {
          "name": "spawn_failure",
          "condition": "Command::new().spawn() returns Err",
          "error_type": "graceful",
          "error_data": {
            "return": "(false, 'Failed to spawn check script: {e}')"
          }
        },
        {
          "name": "timeout_exceeded",
          "condition": "child.wait_timeout returns Ok(None)",
          "error_type": "graceful",
          "error_data": {
            "return": "(false, 'Check script timed out after {timeout_secs}s')",
            "side_effect": "kills child process"
          }
        },
        {
          "name": "wait_error",
          "condition": "child.wait_timeout returns Err",
          "error_type": "graceful",
          "error_data": {
            "return": "(false, 'Error waiting for check script: {e}')"
          }
        }
      ],
      "preconditions": [
        "config.check is non-empty",
        "config.timeout is positive integer"
      ],
      "postconditions": [
        "Returns (true, '') if script exits 0 within timeout",
        "Returns (false, stderr_text) if script exits non-zero",
        "Returns (false, timeout_message) if script exceeds timeout",
        "stderr output truncated to 500 bytes max"
      ],
      "idempotent": false,
      "is_async": false,
      "side_effects": [
        "Resolves script path via policy::resolve_ensure_script_path",
        "Checks file existence",
        "Spawns external process",
        "Inherits stdin, pipes stderr, nulls stdout",
        "Kills child process on timeout",
        "Reads stderr from child"
      ],
      "structured_side_effects": [
        {
          "kind": "reads_file",
          "target": "check script file",
          "description": "Checks existence via script_path.exists()"
        },
        {
          "kind": "network_call",
          "target": "external process",
          "description": "Spawns and waits for check script execution"
        }
      ],
      "performance_budget": {
        "p95_latency_ms": 30000,
        "max_memory_mb": null,
        "complexity": "O(1)"
      }
    },
    {
      "name": "resolve_ensure_result",
      "description": "Converts an Ensure evaluation result to Allow or Deny by running the ensure check script. If passed, converts to Allow; if failed, converts to Deny with message from config and stderr.",
      "inputs": [
        {
          "name": "result",
          "type_ref": "EvaluationResult",
          "required": true,
          "default": "",
          "description": "Evaluation result with decision == Ensure and ensure_config populated",
          "validators": []
        }
      ],
      "output_type": "EvaluationResult",
      "error_cases": [
        {
          "name": "missing_ensure_config",
          "condition": "result.ensure_config.is_none()",
          "error_type": "graceful",
          "error_data": {
            "return": "Deny with reason 'Ensure rule missing ensure config'"
          }
        },
        {
          "name": "check_failed",
          "condition": "resolve_ensure returns (false, stderr)",
          "error_type": "graceful",
          "error_data": {
            "return": "Deny with reason '{config.message} -- {stderr}' or 'Ensure check {check} failed' if message empty"
          }
        }
      ],
      "preconditions": [
        "result.decision == Decision::Ensure implies result.ensure_config.is_some()"
      ],
      "postconditions": [
        "If ensure_config present and check passes: returns Allow with ensure_config cleared",
        "If ensure_config present and check fails: returns Deny with reason from config.message + stderr, ensure_config cleared",
        "If ensure_config missing: returns Deny with reason 'Ensure rule missing ensure config'"
      ],
      "idempotent": false,
      "is_async": false,
      "side_effects": [
        "Calls resolve_ensure which spawns external process"
      ],
      "structured_side_effects": [
        {
          "kind": "network_call",
          "target": "external check script",
          "description": "Executes ensure check 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": [
      "CompiledPolicy",
      "Decision",
      "EnsureConfig",
      "EvaluationResult",
      "ToolCall",
      "evaluate",
      "evaluate_condition",
      "resolve_ensure_script_path"
    ],
    "crate::vault": [
      "Vault",
      "Preflight",
      "PreflightViolation",
      "SoftConstraint",
      "is_disabled_file",
      "is_paused_file"
    ],
    "serde_json": [
      "Value",
      "from_str",
      "to_string"
    ],
    "regex": [
      "Regex::new"
    ],
    "wait_timeout": [
      "ChildExt::wait_timeout"
    ]
  },
  "invariants": [
    "run_hook always returns exit code 0 regardless of decision",
    "Exactly one JSON response emitted to stdout per run_hook invocation",
    "hook_event_name is always 'PreToolUse'",
    "permission_decision is one of: allow, deny, ask",
    "Disabled mode (is_disabled_file) bypasses all policy evaluation",
    "Paused mode (is_paused_file) enforces only locked rules",
    "Hard deny (Decision::Deny) short-circuits preflight soft constraint evaluation",
    "Ensure decisions are always resolved to Allow or Deny before final decision",
    "Preflight escalated state forces Decision::Ask for all tool calls",
    "Soft constraint violations are logged before decision is made",
    "Vault logging occurs after final decision is determined",
    "ensure check timeout clamped to [1, 30] seconds",
    "stderr from ensure checks truncated to 500 bytes",
    "parameters_summary in preflight violations truncated to 200 bytes",
    "action detail logged to vault truncated to 500 bytes",
    "Violation list in escalated preflight context limited to first 20 violations",
    "Tool parameters default to empty JSON object if None",
    "Amount extracted from parameters defaults to 0.0 if missing or non-numeric",
    "Category extracted from parameters defaults to empty string if missing"
  ],
  "requires": [],
  "processing_register": "",
  "data_access": {
    "reads": [
      "stdin",
      "filesystem (disable/pause marker files)",
      "filesystem (ensure check scripts)"
    ],
    "writes": [
      "stdout",
      "vault (action logs, preflight violation logs)"
    ],
    "rationale": "Hook reads tool call JSON from stdin, checks file-based flags, executes external scripts, logs decisions to vault, and writes decision JSON to stdout.",
    "side_effects": [
      {
        "type": "reads_file",
        "classification": "input",
        "fields": [
          "stdin"
        ],
        "rationale": "Primary input mechanism for Claude Code hook integration"
      },
      {
        "type": "writes_file",
        "classification": "output",
        "fields": [
          "stdout"
        ],
        "rationale": "Primary output mechanism for hook decision responses"
      },
      {
        "type": "reads_file",
        "classification": "control",
        "fields": [
          "disable_marker",
          "pause_marker"
        ],
        "rationale": "File-based feature flags for disable and pause modes"
      },
      {
        "type": "network_call",
        "classification": "external_process",
        "fields": [
          "ensure_check_scripts"
        ],
        "rationale": "Spawns and waits for external check scripts with timeout"
      },
      {
        "type": "mutates_state",
        "classification": "persistent",
        "fields": [
          "vault.action_logs",
          "vault.preflight_violations"
        ],
        "rationale": "Logs all decisions and preflight violations for audit trail"
      }
    ]
  },
  "authority": {
    "domains": [],
    "rationale": null
  }
}