#!/usr/bin/env python3 """Record the shape of a hook payload, never its contents. `plan/harnesses.md` lists what this project takes from a harness and marks several entries inferred: they were seen on the headless CLI and assumed to hold in an editor extension, which is the only surface this deployment actually has. This closes that by observing rather than assuming. What lands in the log is field *names*, not field values. A payload carries prompts, tool arguments and file paths; none of that is needed to answer which events fire and what they carry, so none of it is written down. The exceptions are three opaque identifiers -- the session, the agent and the tool call -- which are what the join in `plan/cred-delivery.md` is built on, and a handful of scalars that name a mechanism rather than describe work. """ import json import os import sys import time # Values safe to record: identifiers with no content in them, and names of # mechanisms. Everything else in a payload is the session's own material. SCALARS = ( "hook_event_name", "session_id", "agent_id", "agent_type", "tool_use_id", "tool_name", "permission_mode", "source", "prompt_id", ) # Whether these are set, and never what they hold: the first two are what # `plan/cred-delivery.md` publishes a socket path through, and the last says # which surface the session is running on. ENV = ( "CLAUDE_ENV_FILE", "CLAUDE_PLUGIN_DATA", "CLAUDE_PLUGIN_ROOT", "CLAUDE_CODE_ENTRYPOINT", "CLAUDE_PROJECT_DIR", ) # The label of the run this event belongs to, so rows from a matrix of runs # can be told apart in one log. RUN = "DIDBOT_PROBE_RUN" def log_path(): base = os.environ.get("CLAUDE_PLUGIN_DATA") or os.environ.get("TMPDIR") or "/tmp" return os.path.join(base, "didbot-probe.jsonl") def effort(payload): """`effort` arrives as an object on some events and is absent on others.""" value = payload.get("effort") if isinstance(value, dict): return value.get("level") return value if isinstance(value, str) else None def main(): try: payload = json.load(sys.stdin) except Exception: payload = {} if not isinstance(payload, dict): payload = {} record = { "at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), # Fractional, because comparing when a hook fired against when the # harness wrote a file is a question about tenths of a second. "epoch": round(time.time(), 3), "keys": sorted(payload), "effort": effort(payload), "env": {name: name in os.environ for name in ENV}, # A `SessionStart` hook is handed a path whose file does not exist yet, # so presence of the variable and existence of the file are different # facts and both are worth having. "env_file_exists": os.path.exists(os.environ.get("CLAUDE_ENV_FILE", "")), "run": os.environ.get(RUN, ""), } # A subagent's own transcript is written at a path derived from its id. # An internal fork of the harness is given an id and no transcript, so # the file's existence tells a dispatched agent from a fork -- see # `plan/harnesses.md`. agent_transcript = payload.get("agent_transcript_path") if isinstance(agent_transcript, str) and agent_transcript: record["agent_transcript_exists"] = os.path.exists(agent_transcript) for name in SCALARS: value = payload.get(name) if isinstance(value, (str, int, bool)): record[name] = value path = log_path() try: os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "a", encoding="utf-8") as handle: handle.write(json.dumps(record, sort_keys=True) + "\n") except OSError: pass if __name__ == "__main__": main()