app
Shotlog - Screenshot Log
Public Made by Adomby adom
See the shots your AI never showed you, and know it actually SAW them. adom-shotlog streams every screenshot the AI gathers into a live gallery (webview/pup/browser/phone), reports whether a human is really watching and on which surface, and ingests Adom Bridge child-window arrays via --meta.
master
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
#!/bin/bash
# PostToolUse(SendUserFile) hook - SHIPPED AND MANAGED BY adom-shotlog.
# When an AI SENDS an image to the human with a caption, that caption becomes
# the description of the shot made from the same file (matched by content via
# `shotlog annotate`), so the shotlog card says what the AI said about it -
# not just "Auto-captured screenshot Claude read for analysis: <file>".
# An image that was sent but never Read is injected with the caption.
# Installed by `adom-shotlog install` and re-verified at every
# `adom-shotlog serve` startup; edits here are overwritten.
#
# Reads the hook JSON on stdin:
# {"tool_name":"SendUserFile","tool_input":{"files":[...],"caption":"..."},"cwd":"..."}
# Always exits 0 so it can never block a send.
# Same switch as the read-hook: auto-capture off in the viewer's Preferences.
[ -f "$HOME/.claude/hooks/.shotlog-read-hook-off" ] && exit 0
INPUT="$(cat)"
[ "$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)" = "SendUserFile" ] || exit 0
CAPTION="$(echo "$INPUT" | jq -r '.tool_input.caption // empty' 2>/dev/null)"
# No caption: nothing to carry over (the file name is all the read-hook had too).
[ -z "$(printf '%s' "$CAPTION" | tr -d '[:space:]')" ] && exit 0
command -v shotlog >/dev/null 2>&1 || exit 0
CWD="$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null)"
# Thread + context exactly as the read-hook derives them (used only where the
# shot has none, or when the image was never read and is injected here).
SID="$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null)"
TP="$(echo "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null)"
THREAD=""; CONTEXT=""
if [ -n "$TP" ] && [ -f "$TP" ]; then
THREAD="$(grep '"type":"custom-title"' "$TP" 2>/dev/null | tail -1 | jq -r '.customTitle // empty' 2>/dev/null)"
[ -z "$THREAD" ] && THREAD="$(grep '"type":"ai-title"' "$TP" 2>/dev/null | tail -1 | jq -r '.aiTitle // empty' 2>/dev/null)"
CONTEXT="$(grep '"type":"last-prompt"' "$TP" 2>/dev/null | tail -1 | jq -r '.lastPrompt // empty' 2>/dev/null | head -c 220)"
fi
[ -z "$THREAD" ] && [ -n "$SID" ] && THREAD="thread ${SID:0:8}"
shotlog health >/dev/null 2>&1 || { (cd "${SHOTLOG_HOME:-$HOME/project}" && shotlog serve >/dev/null 2>&1 &) ; sleep 1; }
echo "$INPUT" | jq -r '.tool_input.files[]? // empty' 2>/dev/null | while IFS= read -r f; do
[ -z "$f" ] && continue
case "$f" in /*) ;; *) [ -n "$CWD" ] && f="$CWD/$f" ;; esac
case "$f" in
*.png|*.jpg|*.jpeg|*.PNG|*.JPG|*.JPEG) ;; # the same types the read-hook captures
*) continue ;;
esac
[ -f "$f" ] || continue
# A file already inside shotlog's own store is a stored COPY (possibly
# resized): its fingerprint never matches the original, so --inject-missing
# would add a duplicate card. Same skip as the read-hook.
case "$f" in */screenshots/shotlog/*) continue ;; esac
for attempt in 1 2 3; do
shotlog annotate -c read-captures --inject-missing -d "$CAPTION" \
${THREAD:+--thread "$THREAD"} ${CONTEXT:+--context "$CONTEXT"} "$f" >/dev/null 2>&1 && break
sleep 2
done
done
exit 0