Created
August 16, 2026 06:47
-
-
Save jimmyislive/883238a86c4f8ffe5603c7e3e8d1e903 to your computer and use it in GitHub Desktop.
Obsidian session logger
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| Claude Code -> Obsidian session logger. | |
| Called by Claude Code hooks (see settings.json snippet). Reads hook JSON | |
| from stdin, writes one markdown note per event into a per-session folder | |
| in your Obsidian vault, and chains them together with wikilinks. | |
| Usage (set by the hook command): | |
| python3 log_event.py prompt # UserPromptSubmit | |
| python3 log_event.py tool_call # PreToolUse | |
| python3 log_event.py result # PostToolUse | |
| """ | |
| import sys, json, os, re, datetime, pathlib | |
| # --- CONFIGURE THIS --- | |
| VAULT = pathlib.Path(os.path.expanduser("~/ObsidianVaults/ClaudeSessions")) | |
| # ----------------------- | |
| def slugify(s, maxlen=40): | |
| s = re.sub(r"[^a-zA-Z0-9._-]+", "-", s).strip("-") | |
| return s[:maxlen] or "note" | |
| def load_state(session_dir): | |
| p = session_dir / ".state.json" | |
| if p.exists(): | |
| try: | |
| return json.loads(p.read_text()) | |
| except json.JSONDecodeError: | |
| pass | |
| return {"seq": 0, "last_note": None, "pending": {}} | |
| def save_state(session_dir, state): | |
| (session_dir / ".state.json").write_text(json.dumps(state)) | |
| def append_index(session_dir, note_name, kind): | |
| idx = session_dir / "_session-index.md" | |
| if not idx.exists(): | |
| idx.write_text("---\ntype: session-index\ntags: [session-index]\n---\n\n# Session log\n\n") | |
| with idx.open("a") as f: | |
| f.write(f"- [[{note_name}]] — {kind}\n") | |
| def write_note(session_dir, note_name, frontmatter, body): | |
| fm = "\n".join(f"{k}: {v}" for k, v in frontmatter.items()) | |
| (session_dir / f"{note_name}.md").write_text(f"---\n{fm}\n---\n\n{body}\n") | |
| def main(): | |
| if len(sys.argv) < 2: | |
| sys.exit(0) # never block the session over a bad invocation | |
| kind = sys.argv[1] | |
| try: | |
| data = json.load(sys.stdin) | |
| except json.JSONDecodeError: | |
| sys.exit(0) | |
| session_id = data.get("session_id", "unknown") | |
| date = datetime.date.today().isoformat() | |
| session_dir = VAULT / f"{date}-{session_id[:8]}" | |
| session_dir.mkdir(parents=True, exist_ok=True) | |
| state = load_state(session_dir) | |
| state["seq"] += 1 | |
| seq = state["seq"] | |
| ts = datetime.datetime.now().isoformat(timespec="seconds") | |
| if kind == "prompt": | |
| prompt = data.get("prompt", "") | |
| note_name = f"{seq:03d}-prompt" | |
| links = "\n\n## Links\n" | |
| if state["last_note"]: | |
| links += f"- Previous: [[{state['last_note']}]]\n" | |
| write_note( | |
| session_dir, note_name, | |
| {"type": "prompt", "seq": seq, "session": session_id, "timestamp": ts, "tags": "[prompt]"}, | |
| prompt + links, | |
| ) | |
| append_index(session_dir, note_name, "prompt") | |
| state["last_note"] = note_name | |
| elif kind == "tool_call": | |
| tool_name = data.get("tool_name", "unknown_tool") | |
| tool_input = data.get("tool_input", {}) | |
| tool_use_id = data.get("tool_use_id", "") | |
| note_name = f"{seq:03d}-call-{slugify(tool_name)}" | |
| body = f"**Tool:** `{tool_name}`\n\n```json\n{json.dumps(tool_input, indent=2)}\n```" | |
| links = "\n\n## Links\n" | |
| if state["last_note"]: | |
| links += f"- Triggered by: [[{state['last_note']}]]\n" | |
| write_note( | |
| session_dir, note_name, | |
| {"type": "tool_call", "seq": seq, "session": session_id, "timestamp": ts, | |
| "tool": tool_name, "tags": "[tool_call]"}, | |
| body + links, | |
| ) | |
| append_index(session_dir, note_name, f"tool_call: {tool_name}") | |
| state["last_note"] = note_name | |
| if tool_use_id: | |
| state["pending"][tool_use_id] = note_name | |
| elif kind == "result": | |
| tool_name = data.get("tool_name", "unknown_tool") | |
| tool_use_id = data.get("tool_use_id", "") | |
| resp = data.get("tool_response", "") | |
| resp_str = resp if isinstance(resp, str) else json.dumps(resp, indent=2) | |
| if len(resp_str) > 4000: | |
| resp_str = resp_str[:4000] + "\n... [truncated]" | |
| note_name = f"{seq:03d}-result-{slugify(tool_name)}" | |
| body = f"**Tool:** `{tool_name}`\n\n```\n{resp_str}\n```" | |
| links = "\n\n## Links\n" | |
| call_note = state["pending"].pop(tool_use_id, None) | |
| if call_note: | |
| links += f"- Produced by: [[{call_note}]]\n" | |
| elif state["last_note"]: | |
| links += f"- Previous: [[{state['last_note']}]]\n" | |
| write_note( | |
| session_dir, note_name, | |
| {"type": "result", "seq": seq, "session": session_id, "timestamp": ts, | |
| "tool": tool_name, "tags": "[result]"}, | |
| body + links, | |
| ) | |
| append_index(session_dir, note_name, f"result: {tool_name}") | |
| state["last_note"] = note_name | |
| save_state(session_dir, state) | |
| sys.exit(0) | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment