app
Adom Shotlog
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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
#!/bin/bash
# PostToolUse(Read) hook - SHIPPED AND MANAGED BY adom-shotlog.
# Deterministically injects any image file Claude Reads into the shared
# 'read-captures' channel, so the human sees every screenshot any AI analyzed,
# without the AI having to remember. 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":"Read","tool_input":{"file_path":"..."}}
# - Only acts on .png/.jpg/.jpeg
# - Skips files already inside a shotlog data dir (no re-inject loops)
# - Self-heals the broker from the canonical cwd if it is not running
# Always exits 0 so it can never block a Read.
# User preference: the read-hook can be toggled OFF in the viewer's
# Preferences dialog (broker writes this flag file). No-op when disabled.
[ -f "$HOME/.claude/hooks/.shotlog-read-hook-off" ] && exit 0
INPUT="$(cat)"
f="$(echo "$INPUT" | jq -r 'select(.tool_name=="Read") | (.tool_input.file_path // empty)' 2>/dev/null)"
[ -z "$f" ] && exit 0
case "$f" in
*.png|*.jpg|*.jpeg|*.PNG|*.JPG|*.JPEG) ;;
*) exit 0 ;;
esac
case "$f" in
*/screenshots/shotlog/*) exit 0 ;;
esac
[ -f "$f" ] || exit 0
command -v shotlog >/dev/null 2>&1 || exit 0
# Self-heal the broker from the canonical project dir (a stray cwd once
# scattered data dirs across the container).
shotlog health >/dev/null 2>&1 || { (cd "${SHOTLOG_HOME:-$HOME/project}" && shotlog serve >/dev/null 2>&1 &) ; sleep 1; }
base="$(basename "$f")"
# Attribute the capture to the AI THREAD that read the image: the hook payload
# carries session_id + transcript_path; the transcript's ai-title records hold
# the thread's human-readable title.
SID="$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null)"
TP="$(echo "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null)"
THREAD=""
if [ -n "$TP" ] && [ -f "$TP" ]; then
# The human's manual rename (custom-title) WINS over the AI-generated name.
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)"
fi
[ -z "$THREAD" ] && [ -n "$SID" ] && THREAD="thread ${SID:0:8}"
# WHY the shot exists: the user's current prompt (last-prompt record), pure
# grep/jq - zero AI tokens.
CONTEXT=""
if [ -n "$TP" ] && [ -f "$TP" ]; then
CONTEXT="$(grep '"type":"last-prompt"' "$TP" 2>/dev/null | tail -1 | jq -r '.lastPrompt // empty' 2>/dev/null | head -c 220)"
fi
# PROVENANCE: infer the capture endpoint from the path bucket, and attach the
# AD sidecar (coordMap, child windows, orig size) when this is an AD capture.
SRC="read-hook"
META_ARGS=""
case "$f" in
*/screenshots/adom-desktop/window/*) SRC="read-hook/ad-window" ;;
*/screenshots/adom-desktop/screen/*) SRC="read-hook/ad-screen" ;;
*/adom-desktop-screenshots/*) SRC="read-hook/ad-window" ;;
*pup*|*/browser-shots/*) SRC="read-hook/pup" ;;
*/recordings/*|*hydrogen*) SRC="read-hook/hydrogen" ;;
esac
SIDECAR="${f%.safe.png}.json"; [ "$SIDECAR" = "$f" ] && SIDECAR="${f%.png}.json"
if [ -f "$SIDECAR" ] && grep -q "coordMap\|origWidth" "$SIDECAR" 2>/dev/null; then
META_ARGS="--meta $SIDECAR"
fi
# Satisfy shotlog's original-resolution contract (a blind pre-shrunk inject is
# REJECTED). AD saves resized captures as *.safe.png beside the *.full.png
# original - pass the original when it exists; otherwise the file Claude read
# IS the artifact of record, so --native is honest.
ORIG_ARGS="--native"
case "$f" in
*.safe.png)
# full sibling naming differs by bucket: screen saves <stem>.full.png, window saves <stem>.png
for full in "${f%.safe.png}.full.png" "${f%.safe.png}.png"; do
[ -f "$full" ] && { ORIG_ARGS="--orig-file $full"; break; }
done
;;
esac
# Inject with RETRY: a broker deploy/restart leaves a ~2-3s window where the
# inject would hit connection-refused - never silently drop a capture for that.
for attempt in 1 2 3; do
if shotlog inject -c read-captures \
-d "Auto-captured screenshot Claude read for analysis: ${base}" \
-s "$SRC" ${THREAD:+--thread "$THREAD"} ${CONTEXT:+--context "$CONTEXT"} $META_ARGS $ORIG_ARGS "$f" >/dev/null 2>&1; then
break
fi
sleep 2
# broker may need a nudge after a failed first try
shotlog health >/dev/null 2>&1 || { (cd "${SHOTLOG_HOME:-$HOME/project}" && shotlog serve >/dev/null 2>&1 &) ; sleep 1; }
done
exit 0