#!/usr/bin/env python3 """Tell a Claude Code session what Tangled is and how it signs in to atgc. Runs at SessionStart and prints a few lines of context, then nothing for the rest of the session: `atgc agent` is the full briefing, and this only gets an agent to the point of running it. The lines cover what the briefing does not: which account to sign in as, that `atgc auth login` blocks until the sign-in is approved, and that the account has to be named on every command. **When it prints.** Only when `atgc` is on `PATH`. Without it the hook prints nothing, and it never names an install command: a hook's output is an instruction to the agent, and installing software is the human's call. The plugin's README says how to install atgc. **Whose account.** On a machine running didbot, each session gets an agent identity of its own, and its DID reaches the agent in didbot's hook output on its first tool call. This hook runs before that and cannot see it, so it says where the DID appears and writes `` in its place. didbot counts as present when the `didbot` command is on `PATH` and its agent daemon's socket exists, which is how didbot's own hook and vibescrobble's find it. Without didbot, the session belongs to whoever runs it, and the text drops the sign-in lines. **Why the account is named on every command.** atgc treats a Claude Code session as an agent, and an agent never falls back to the default account. A linked worktree does not read the account `atgc repo configure` set in the main checkout, and the session's shell does not keep an exported variable from one command to the next, so `--account` or `ATGC_ACCOUNT` goes on each command. The hook always exits 0, so a fault in it never blocks a session. """ import json import os import shutil import stat import sys TANGLED = ( "Tangled (tangled.org) is a git forge built on atproto. Repositories are " "hosted on knot servers; pull requests, issues and comments are records " "in each author's own atproto account. `atgc` is its command-line client." ) SIGN_IN = ( "You can sign in to atgc as the agent identity didbot gives this " "session, and open pull requests as yourself. Your DID appears in " "didbot's hook output. The login waits until the sign-in is approved, so " "run it in the background:\n" "\n" " atgc auth login \n" "\n" "didbot then shows the sign-in request; approve it with the " "`didbot oauth approve` command it gives." ) ACCOUNT = ( "Name the account on every atgc command, with `--account {who}` or " "`ATGC_ACCOUNT={who}` in front of it. A linked worktree does not read " "the account `atgc repo configure` set in the main checkout, and a " "variable exported in one shell command is gone by the next." ) PATCH_ONLY = ( "Without push access to a repository, open the pull request with " "`atgc pr create --patch-only`, which needs no SSH key." ) BRIEFING = "Run `atgc agent` for everything else." def daemon_socket(): """Where the didbot agent daemon listens, as didbot's own hook finds it.""" override = os.environ.get("DIDBOT_SOCK") if override: return override runtime = os.environ.get("XDG_RUNTIME_DIR") or "/tmp" return os.path.join(runtime, "didbot-agent", "agent.sock") def has_didbot(): """Whether didbot gives this session an agent identity.""" if shutil.which("didbot") is None: return False try: return stat.S_ISSOCK(os.stat(daemon_socket()).st_mode) except OSError: return False def context(): """The text for this session, or `None` when atgc is not installed.""" if shutil.which("atgc") is None: return None if has_didbot(): parts = [TANGLED, SIGN_IN, ACCOUNT.format(who="")] else: parts = [TANGLED, ACCOUNT.format(who="")] parts += [PATCH_ONLY, BRIEFING] return "\n\n".join(parts) def main(): text = context() if text is None: return output = {"hookEventName": "SessionStart", "additionalContext": text} print(json.dumps({"hookSpecificOutput": output})) if __name__ == "__main__": try: main() except Exception: pass sys.exit(0)