Created
March 2, 2026 05:57
-
-
Save fizz/530817bdc735be5690e5f0609c74db3c to your computer and use it in GitHub Desktop.
Claude Code PostToolUse hook: log every Bash command to atuin (shell history) and WakaTime (activity tracker).
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 PostToolUse hook: log Bash commands to atuin + wakatime. | |
| Every command Claude runs via the Bash tool gets: | |
| - Recorded in atuin so it appears in your shell history (`atuin history list`) | |
| - Sent to WakaTime as a heartbeat so AI-assisted coding shows up in your dashboard | |
| Fire-and-forget — never blocks Claude on logging failures. | |
| Setup (in ~/.claude/settings.json): | |
| "hooks": { | |
| "PostToolUse": [ | |
| { | |
| "matcher": "Bash", | |
| "hooks": [{ "type": "command", "command": "/path/to/posttool_bash_history.py" }] | |
| } | |
| ] | |
| } | |
| Requires: atuin, wakatime CLI (both optional — silently skips if not installed) | |
| """ | |
| import json | |
| import sys | |
| import subprocess | |
| import os | |
| def main(): | |
| data = json.load(sys.stdin) | |
| if data.get("tool_name") != "Bash": | |
| print(json.dumps({"decision": "approve"})) | |
| return | |
| command = data.get("tool_input", {}).get("command", "") | |
| cwd = data.get("cwd", os.getcwd()) | |
| interrupted = data.get("tool_response", {}).get("interrupted", False) | |
| if not command.strip(): | |
| print(json.dumps({"decision": "approve"})) | |
| return | |
| exit_code = 1 if interrupted else 0 | |
| # --- Atuin: record command in shell history --- | |
| try: | |
| env = os.environ.copy() | |
| env["ATUIN_SESSION"] = data.get("session_id", "claude-code") | |
| result = subprocess.run( | |
| ["atuin", "history", "start", "--", command], | |
| capture_output=True, text=True, timeout=5, | |
| cwd=cwd, env=env, | |
| ) | |
| history_id = result.stdout.strip() | |
| if history_id: | |
| subprocess.Popen( | |
| ["atuin", "history", "end", "--exit", str(exit_code), history_id], | |
| cwd=cwd, env=env, | |
| stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, | |
| ) | |
| except Exception: | |
| pass | |
| # --- WakaTime: send heartbeat for AI coding activity --- | |
| try: | |
| try: | |
| git_root = subprocess.run( | |
| ["git", "-C", cwd, "rev-parse", "--show-toplevel"], | |
| capture_output=True, text=True, timeout=3, | |
| ) | |
| project = ( | |
| os.path.basename(git_root.stdout.strip()) | |
| if git_root.returncode == 0 | |
| else os.path.basename(cwd) | |
| ) | |
| except Exception: | |
| project = os.path.basename(cwd) | |
| try: | |
| git_branch = subprocess.run( | |
| ["git", "-C", cwd, "branch", "--show-current"], | |
| capture_output=True, text=True, timeout=3, | |
| ) | |
| branch = git_branch.stdout.strip() if git_branch.returncode == 0 else None | |
| except Exception: | |
| branch = None | |
| waka_cmd = [ | |
| "wakatime", "--write", | |
| "--entity-type", "app", | |
| "--entity", "Claude Code", | |
| "--project", project, | |
| "--language", "Bash", | |
| "--category", "ai coding", | |
| ] | |
| if branch: | |
| waka_cmd.extend(["--alternate-branch", branch]) | |
| subprocess.Popen( | |
| waka_cmd, | |
| stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, | |
| ) | |
| except Exception: | |
| pass | |
| print(json.dumps({"decision": "approve"})) | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment