#!/bin/bash # Emit the canonical session-log "## User" attribution block. # # ATTRIBUTION IS NEVER INFERRED. The only sources of truth are: # .claude/identity.json (per-machine, gitignored — who sits at this keyboard) # .claude/users.json (role lookup by user key) # Do NOT derive the user from the hostname, the `# userEmail` context hint, or # from memory. /save calls this script and pastes its output verbatim as the # session log's first section, so every log is stamped identically and correctly. set -e REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) ID="$REPO_ROOT/.claude/identity.json" USERS="$REPO_ROOT/.claude/users.json" if [ ! -f "$ID" ]; then echo "## User" echo "- **User:** UNKNOWN — no .claude/identity.json on this machine." echo "- **Action:** run the first-machine onboarding flow in CLAUDE.md before saving." exit 0 fi PYTHON="" for c in py python3 python; do if command -v "$c" >/dev/null 2>&1 && "$c" -c "import sys" >/dev/null 2>&1; then PYTHON="$c"; break; fi done if [ -z "$PYTHON" ]; then echo "## User" echo "- **User:** (could not render — no Python interpreter found)" exit 0 fi # Bot-context override: the Discord bot sets CLAUDETOOLS_ACTOR=discord-bot plus # the requester it is acting for (CLAUDETOOLS_REQUESTER / _USER, per session). # Attribute the log to the BOT as executor and the human requester as originator. # Strict no-op when the env is unset — interactive sessions are unaffected. if [ "${CLAUDETOOLS_ACTOR:-}" = "discord-bot" ]; then "$PYTHON" - "$ID" "$USERS" <<'BOTEOF' import json, os, sys idp, usersp = sys.argv[1], sys.argv[2] try: machine = json.load(open(idp)).get("machine", "unknown") except Exception: machine = "unknown" requester = os.environ.get("CLAUDETOOLS_REQUESTER", "an unrecognized Discord user") ukey = os.environ.get("CLAUDETOOLS_REQUESTER_USER", "") role = "" if ukey: try: role = json.load(open(usersp))["users"].get(ukey, {}).get("role", "") except Exception: pass print("## User") print(f"- **Executed by:** ClaudeTools Discord Bot ({machine})") print(f"- **Requested by:** {requester}" + (f" - {role}" if role else "")) print("- **Role:** automation (acting on the requester's behalf)") BOTEOF exit 0 fi "$PYTHON" - "$ID" "$USERS" <<'PYEOF' import json, sys, socket, re idp, usersp = sys.argv[1], sys.argv[2] try: d = json.load(open(idp)) except Exception as e: print("## User") print(f"- **User:** UNREADABLE — .claude/identity.json failed to parse ({e}).") print("- **Action:** repair identity.json before saving; do not infer the user.") sys.exit(0) full = d.get("full_name") or d.get("user", "unknown") user = d.get("user", "unknown") machine = d.get("machine", "unknown") role = d.get("role", "") if not role: try: role = json.load(open(usersp))["users"].get(user, {}).get("role", "") except Exception: pass def norm(x): return re.sub(r'\.local$', '', (x or '').lower()) host = socket.gethostname() stale = machine and host and norm(machine) != norm(host) print("## User") print(f"- **User:** {full} ({user})") print(f"- **Machine:** {machine}") if role: print(f"- **Role:** {role}") if stale: print(f"- **[WARNING]** identity.json machine '{machine}' != hostname '{host}'. " f"identity.json may be stale on this box — verify before trusting this attribution.") PYEOF