{
  "schema": "https://ai-atoms.com/schemas/hook-v1.json",
  "type": "hook",
  "id": "hook/audit-command",
  "version": "1.0.1",
  "name": "Audit Command Wrapper",
  "description": "PostToolUse hook that records every wrapped command invocation (git, gh, etc.) to the audit log. Makes the appearance side of the audit trail reliable — absence of an audit line for an expected command is itself visible during forensic review. Records WRAPPED_CMD, WRAPPED_ARGV, WRAPPED_EXIT, and WRAPPED_DURATION.",
  "event": "PostToolUse",
  "language": "python",
  "trigger": {
    "type": "always"
  },
  "blocking": false,
  "side_effects": [
    "appends invocation record to ~/.ai/audit/interactions/<YYYY-MM>.jsonl"
  ],
  "authored_by": "convergent-systems-key",
  "tags": [
    "audit",
    "logging",
    "governance",
    "commands",
    "claude-code"
  ],
  "lifecycle": "stable",
  "platforms": [
    "linux",
    "macos",
    "windows"
  ],
  "platform_notes": "Logic is cross-platform. Wiring: use 'ai hooks run audit-command' in settings.json — the ai binary discovers Python on each OS. Reads env vars WRAPPED_CMD etc. — set by the ai command wrappers on all platforms.",
  "script": "#!/usr/bin/env python3\n\"\"\"hooks/audit-command.py — wrapper postHook that records every wrapped\ncommand invocation to the audit log.\n\nPer SPEC.md §10.5.4: \"If a bypass leaves no audit line where one\nwould normally appear, the gap itself is visible.\" This hook makes\nthe appearance side reliable; the absence side is what surfaces in\nforensic review.\n\nInputs (from the wrapper environment):\n  WRAPPED_CMD       — the underlying command (\"git\", \"gh\", etc.)\n  WRAPPED_ARGV      — the argv it was invoked with, JSON-encoded\n  WRAPPED_EXIT      — the exit code of the real command\n  WRAPPED_DURATION  — wall-clock seconds the command took (string)\n\nSelf-check:\n  --self-check\n\"\"\"\nfrom __future__ import annotations\n\nimport argparse\nimport datetime as dt\nimport json\nimport os\nimport sys\nfrom pathlib import Path\n\nsys.path.insert(0, str(Path(__file__).resolve().parent))\nimport _lib  # noqa: E402\n\n\ndef audit_dir() -> Path:\n    root = os.environ.get(\"AI_ROOT\", str(Path.home() / \".ai\"))\n    return Path(root) / \"audit\" / \"interactions\"\n\n\ndef month_file() -> Path:\n    now = dt.datetime.now(dt.timezone.utc)\n    return audit_dir() / f\"{now.strftime('%Y-%m')}.jsonl\"\n\n\ndef chronon() -> str:\n    now = dt.datetime.now(dt.timezone.utc)\n    return now.strftime(\"%Y-%m-%dT%H:%M:%S.\") + f\"{now.microsecond // 1000:03d}Z\"\n\n\ndef main(argv: list[str]) -> int:\n    parser = argparse.ArgumentParser(add_help=True)\n    parser.add_argument(\"--self-check\", action=\"store_true\")\n    # The command wrapper invokes every hook with --mode=wrapper. This hook is\n    # wrapper-only and reads its inputs from the WRAPPED_* env vars, so the flag\n    # is accepted and ignored rather than selecting a code path.\n    parser.add_argument(\"--mode\", choices=[\"claude\", \"wrapper\"], default=None,\n                        help=\"invocation mode (set by the command wrapper)\")\n    args = parser.parse_args(argv)\n    if args.self_check:\n        try:\n            audit_dir().mkdir(parents=True, exist_ok=True)\n        except Exception as e:\n            _lib.log(\"self-check FAIL:\", e)\n            return 1\n        _lib.log(\"self-check OK\")\n        return 0\n\n    cmd = os.environ.get(\"WRAPPED_CMD\", \"\")\n    argv_json = os.environ.get(\"WRAPPED_ARGV\", \"[]\")\n    exit_code = os.environ.get(\"WRAPPED_EXIT\", \"0\")\n    duration = os.environ.get(\"WRAPPED_DURATION\", \"\")\n\n    try:\n        wrapped_argv = json.loads(argv_json)\n    except json.JSONDecodeError:\n        wrapped_argv = []\n\n    # Redact the argv (the secret patterns include URL-with-credentials).\n    redacted_argv = [_lib.redact(str(a)) for a in wrapped_argv]\n    probe_payload = json.dumps(redacted_argv)[:1000]\n\n    event = {\n        \"chronon\": chronon(),\n        \"trace\": os.environ.get(\"AI_SESSION_ID\", \"\"),\n        \"cwd\": os.getcwd(),\n        \"actor\": \"tool\",\n        \"kind\": \"invocation-result\",\n        \"engine\": \"command-wrapper\",\n        \"probe\": cmd,\n        \"probe_payload\": probe_payload,\n        \"emission_marker\": f\"exit={exit_code} duration={duration}\",\n    }\n\n    try:\n        audit_dir().mkdir(parents=True, exist_ok=True)\n        with open(month_file(), \"a\", encoding=\"utf-8\") as f:\n            f.write(json.dumps(event, ensure_ascii=False) + \"\\n\")\n    except Exception as e:\n        # Never block; audit failures are themselves auditable.\n        _lib.log(\"audit-command append failed:\", e)\n    return 0\n\n\nif __name__ == \"__main__\":\n    sys.exit(main(sys.argv[1:]))\n",
  "depends_on": [
    "hook/lib"
  ],
  "category": "governance"
}