#!/usr/bin/env python3 """agent-id — detect which agent harness called this tool. Walks /proc/self/.. up the process tree, inspecting cmdlines, comm names, and ancestor environment variables to identify Claude Code, Codex CLI, or Hermes/pi agent — without the caller passing any flags or env vars. Outputs JSON: {"agent": "...", "session_id": "...", "call_id": "...", "pid": N, "ppid": N, "cmdline": "..."} """ import json import os import re import sys def read_proc_file(pid: int, name: str) -> str | None: """Read /proc//, return stripped content or None.""" try: with open(f"/proc/{pid}/{name}", "rb") as f: raw = f.read() return raw.decode("utf-8", errors="replace").strip("\x00\n ") except (FileNotFoundError, PermissionError, ProcessLookupError): return None def read_cmdline(pid: int) -> str | None: """Read null-joined argv from /proc//cmdline.""" raw = read_proc_file(pid, "cmdline") if raw is None: return None return raw.replace("\x00", " ").strip() def read_comm(pid: int) -> str | None: """Read process comm name from /proc//comm.""" return read_proc_file(pid, "comm") def read_status_field(pid: int, field: str) -> str | None: """Read a specific field value from /proc//status.""" raw = read_proc_file(pid, "status") if raw is None: return None for line in raw.splitlines(): if line.startswith(field + ":"): return line.split(":", 1)[-1].strip() return None def read_ppid(pid: int) -> int | None: """Get parent PID from /proc//status.""" val = read_status_field(pid, "PPid") if val is None: return None try: return int(val) except ValueError: return None def read_environ_dict(pid: int) -> dict[str, str]: """Parse /proc//environ into a dict.""" raw = read_proc_file(pid, "environ") if raw is None: return {} result = {} for entry in raw.split("\x00"): if "=" in entry: k, _, v = entry.partition("=") result[k] = v return result def get_own_environ() -> dict[str, str]: """Get our own environment via /proc/self/environ (not os.environ).""" return read_environ_dict(os.getpid()) def walk_process_tree() -> list[dict]: """Walk up from our PID to init, return ordered list of process info dicts.""" pid = os.getpid() tree = [] visited = set() while pid is not None and pid > 1 and pid not in visited: visited.add(pid) cmdline = read_cmdline(pid) comm = read_comm(pid) env = read_environ_dict(pid) ppid = read_ppid(pid) tree.append({ "pid": pid, "ppid": ppid, "comm": comm, "cmdline": cmdline, "env": env, }) pid = ppid return tree # ── Agent detection patterns ────────────────────────────────────────────── PATTERNS: list[tuple[str, re.Pattern, str]] = [ # (agent_name, cmdline_regex, description) ("hermes", re.compile(r"python.*hermes_cli", re.I), "python -m hermes_cli.main"), ("hermes", re.compile(r"hermes[_-]gateway", re.I), "hermes gateway"), ("claude", re.compile(r"claude", re.I), "claude binary"), ("claude", re.compile(r"claude[_-]code", re.I), "claude-code"), ("codex", re.compile(r"codex\b(?!\s+exec)", re.I), "codex binary"), ("codex", re.compile(r"codex[_-]cli", re.I), "codex-cli"), ] SESSION_ENV_VARS: dict[str, list[str]] = { "hermes": ["HERMES_SESSION_ID"], "claude": [ "CLAUDE_CODE_CHILD_SESSION", "CLAUDE_CODE_SESSION_LOG", "CLAUDE_SESSION_INGRESS_TOKEN_FILE", ], "codex": [ "CODEX_THREAD_ID", "CODEX_EVENT_ID", "CODEX_CALL_ID", ], } CALL_ENV_VARS: dict[str, list[str]] = { "hermes": [], "claude": [], "codex": ["CODEX_CALL_ID", "CODEX_EVENT_ID", "CODEX_TURN_ID"], } def detect_agent(tree: list[dict]) -> dict: """Detect which agent called us by analyzing the process tree.""" self_pid = tree[0]["pid"] if tree else os.getpid() self_cmdline = tree[0]["cmdline"] if tree else None result: dict = { "agent": "unknown", "session_id": None, "call_id": None, "pid": self_pid, "ppid": tree[0]["ppid"] if len(tree) > 0 else None, "cmdline": self_cmdline, "ancestors": [], } for i, node in enumerate(tree): cmdline = node["cmdline"] or "" comm = node["comm"] or "" env = node["env"] ancestor_info = { "pid": node["pid"], "comm": comm, "cmdline_preview": cmdline[:120] + ("..." if len(cmdline) > 120 else ""), "agent_hit": None, } for agent_name, pattern, desc in PATTERNS: if pattern.search(cmdline) or pattern.search(comm): if result["agent"] == "unknown": result["agent"] = agent_name ancestor_info["agent_hit"] = f"{agent_name} ({desc})" break if i > 0 and comm == "hermes": if result["agent"] == "unknown": result["agent"] = "hermes" ancestor_info["agent_hit"] = "hermes (comm=hermes)" for agent_name, var_names in SESSION_ENV_VARS.items(): for var in var_names: if var in env and env[var]: if result["agent"] == "unknown": result["agent"] = agent_name if not result["session_id"]: result["session_id"] = env[var] ancestor_info["agent_hit"] = ( ancestor_info["agent_hit"] or "" ) + f" {agent_name}({var}={env[var][:60]})" break for agent_name, var_names in CALL_ENV_VARS.items(): for var in var_names: if var in env and env[var]: if not result["call_id"]: result["call_id"] = env[var] break result["ancestors"].append(ancestor_info) return result def main(): tree = walk_process_tree() result = detect_agent(tree) output = {k: v for k, v in result.items() if k != "ancestors"} print(json.dumps(output, indent=2)) if __name__ == "__main__": main()