Skip to content

Instantly share code, notes, and snippets.

@fizz
Created March 2, 2026 05:57
Show Gist options
  • Select an option

  • Save fizz/530817bdc735be5690e5f0609c74db3c to your computer and use it in GitHub Desktop.

Select an option

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).
#!/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