2026-10-07 19:26:06 -04:00
|
|
|
#!/bin/bash
|
|
|
|
|
# Claude Code hook (wired in via claude-settings.json) that tracks what Claude is
|
|
|
|
|
# doing in each DevDesk session, for the sidebar's at-a-glance status:
|
|
|
|
|
# UserPromptSubmit, PostToolUse -> working
|
2026-10-10 20:22:37 -04:00
|
|
|
# PermissionRequest -> waiting, with what Claude wants to run
|
|
|
|
|
# (tool + command/file/URL), for the inbox's
|
|
|
|
|
# Allow/Deny (devdesk#22)
|
2026-10-07 19:26:06 -04:00
|
|
|
# Notification -> waiting (Claude needs input or a permission)
|
|
|
|
|
# Stop, SessionStart -> idle
|
|
|
|
|
# SessionEnd -> exited
|
|
|
|
|
# Fires on every tool call, so it stays tiny and only writes when the state changes.
|
2026-10-07 23:34:59 -04:00
|
|
|
# Entering "waiting" also starts devdesk-notify-matrix, which posts to Matrix if
|
|
|
|
|
# the wait lasts and nobody is attached (devdesk#28).
|
2026-10-07 19:26:06 -04:00
|
|
|
|
|
|
|
|
input=$(cat)
|
|
|
|
|
|
|
|
|
|
outer=${TMUX:-}
|
|
|
|
|
[ "${outer%%,*}" = "${DEVDESK_SOCKET:-/run/devdesk/tmux.sock}" ] && [ -n "${TMUX_PANE:-}" ] || exit 0
|
|
|
|
|
session=$(tmux display-message -p -t "$TMUX_PANE" '#S' 2>/dev/null)
|
|
|
|
|
[[ $session =~ ^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$ ]] || exit 0
|
|
|
|
|
|
2026-10-08 01:16:37 -04:00
|
|
|
# Unit separator, not tab: read collapses runs of whitespace delimiters, so an
|
|
|
|
|
# empty field would shift the ones after it.
|
2026-10-11 23:28:11 -04:00
|
|
|
IFS=$'\x1f' read -r event message tool file ntype < <(jq -rM '[.hook_event_name // "", (.message // "" | gsub("[\u001f\n]"; " ")),
|
|
|
|
|
.tool_name // "", (.tool_input.file_path // .tool_input.notebook_path // "" | gsub("[\u001f\n]"; " ")),
|
|
|
|
|
.notification_type // ""] | join("\u001f")' <<<"$input" 2>/dev/null)
|
|
|
|
|
|
|
|
|
|
# Claude's idle reminder ("Claude is waiting for your input", about a minute
|
|
|
|
|
# after a turn ends) is not a request: the turn is simply done. It must not
|
|
|
|
|
# turn the session red or reach Matrix (devdesk#50).
|
|
|
|
|
if [ "$event" = Notification ] &&
|
|
|
|
|
{ [ "$ntype" = idle_prompt ] || { [ -z "$ntype" ] && [[ $message == *"waiting for your input"* ]]; }; }; then
|
|
|
|
|
exit 0
|
|
|
|
|
fi
|
2026-10-08 01:16:37 -04:00
|
|
|
|
|
|
|
|
# Files Claude edits, for the file viewer's "Claude" badge (devdesk#19).
|
|
|
|
|
case $event:$tool in
|
|
|
|
|
PostToolUse:Edit | PostToolUse:Write | PostToolUse:MultiEdit | PostToolUse:NotebookEdit)
|
|
|
|
|
if [[ $file == /* ]]; then
|
|
|
|
|
log="${DEVDESK_STATE_DIR:-/var/lib/devdesk}/edits/$session.txt"
|
|
|
|
|
mkdir -p "${log%/*}" 2>/dev/null
|
|
|
|
|
if ! grep -qxF -- "$file" "$log" 2>/dev/null && [ "$(wc -l <"$log" 2>/dev/null || echo 0)" -lt 1000 ]; then
|
|
|
|
|
printf '%s\n' "$file" >>"$log"
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
;;
|
|
|
|
|
esac
|
2026-10-10 20:37:46 -04:00
|
|
|
# Ops trail (devdesk#27): every Bash command, and the hosts a session works on
|
|
|
|
|
# (ssh targets, Proxmox CT/VM ids) as tags for the session list.
|
|
|
|
|
if [ "$event:$tool" = PostToolUse:Bash ]; then
|
|
|
|
|
cmd=$(jq -rM '.tool_input.command // empty' <<<"$input" 2>/dev/null)
|
|
|
|
|
if [ -n "$cmd" ]; then
|
|
|
|
|
base="${DEVDESK_STATE_DIR:-/var/lib/devdesk}"
|
|
|
|
|
mkdir -p "$base/commands" "$base/tags" 2>/dev/null
|
|
|
|
|
log="$base/commands/$session.jsonl"
|
|
|
|
|
jq -cMn --arg c "${cmd:0:500}" '{at: (now | floor), cmd: $c}' >>"$log" 2>/dev/null
|
|
|
|
|
if [ "$(wc -l <"$log" 2>/dev/null || echo 0)" -gt 600 ]; then
|
|
|
|
|
tail -n 500 "$log" >"$log.tmp" 2>/dev/null && mv -f "$log.tmp" "$log"
|
|
|
|
|
fi
|
|
|
|
|
hosts=$(
|
|
|
|
|
# ssh [options] [user@]host: the first word that isn't an option (or an option's value)
|
|
|
|
|
awk '{ for (i = 1; i <= NF; i++) if ($i == "ssh") { j = i + 1
|
|
|
|
|
while (j <= NF && $j ~ /^-/) { if ($j ~ /^-[bcDEeFIiJLlmOopQRSWw]$/) j++; j++ }
|
|
|
|
|
if (j <= NF) { h = $j; sub(/^[^@]*@/, "", h); gsub(/[^A-Za-z0-9_.-].*$/, "", h); if (h != "") print h } } }' <<<"$cmd"
|
|
|
|
|
grep -oE '\bpct[[:space:]]+(exec|enter|push|pull|config|set|start|stop|shutdown|reboot|status|snapshot|rollback|migrate)[[:space:]]+[0-9]{3,}' <<<"$cmd" | grep -oE '[0-9]+$' | sed 's/^/CT /'
|
|
|
|
|
grep -oE '\bqm[[:space:]]+(guest[[:space:]]+exec|terminal|config|set|start|stop|shutdown|reboot|status|snapshot|rollback|migrate)[[:space:]]+[0-9]{3,}' <<<"$cmd" | grep -oE '[0-9]+$' | sed 's/^/VM /'
|
|
|
|
|
)
|
|
|
|
|
if [ -n "$hosts" ]; then
|
|
|
|
|
tags="$base/tags/$session.json"
|
|
|
|
|
jq -cMn --rawfile new <(printf '%s\n' "$hosts") --slurpfile old <(cat "$tags" 2>/dev/null || echo '{}') '
|
|
|
|
|
($new | split("\n") | map(select(length > 0))) as $n
|
|
|
|
|
| {hosts: ($n + ($old[0].hosts // []) | reduce .[] as $h ([]; if index([$h]) then . else . + [$h] end) | .[:4]),
|
|
|
|
|
at: (now | floor)}' >"$tags.tmp" 2>/dev/null && mv -f "$tags.tmp" "$tags"
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
2026-10-07 19:26:06 -04:00
|
|
|
case $event in
|
|
|
|
|
UserPromptSubmit | PostToolUse) state=working ;;
|
2026-10-10 20:22:37 -04:00
|
|
|
Notification | PermissionRequest) state=waiting ;;
|
2026-10-07 19:26:06 -04:00
|
|
|
Stop | SessionStart) state=idle ;;
|
|
|
|
|
SessionEnd) state=exited ;;
|
|
|
|
|
*) exit 0 ;;
|
|
|
|
|
esac
|
|
|
|
|
|
|
|
|
|
dir="${DEVDESK_STATE_DIR:-/var/lib/devdesk}/state"
|
|
|
|
|
file="$dir/$session.json"
|
|
|
|
|
# Unchanged state (the common case during tool runs): nothing to write.
|
|
|
|
|
if [ "$state" != waiting ] && [ -f "$file" ] && grep -q "\"state\":\"$state\"" "$file"; then
|
|
|
|
|
exit 0
|
|
|
|
|
fi
|
2026-10-10 20:22:37 -04:00
|
|
|
# What a permission prompt is about. tool_use_id is null in PermissionRequest,
|
|
|
|
|
# so a wait is identified by its `since` instead.
|
|
|
|
|
request=null
|
|
|
|
|
if [ "$event" = PermissionRequest ]; then
|
|
|
|
|
request=$(jq -cM '(.tool_input // {}) as $i | {tool: (.tool_name // "a tool"),
|
|
|
|
|
detail: (($i.command // $i.file_path // $i.notebook_path // $i.url // $i.pattern // $i.query // $i.path // ($i | tostring)) | tostring | .[0:600]),
|
|
|
|
|
description: ($i.description // null)}' <<<"$input" 2>/dev/null) || request=null
|
|
|
|
|
message="Claude wants to use ${tool:-a tool}"
|
|
|
|
|
fi
|
2026-10-07 23:34:59 -04:00
|
|
|
# A second Notification during the same wait updates the message but keeps
|
2026-10-10 20:22:37 -04:00
|
|
|
# `since` (and what the wait is about), so the wait stays one wait (one Matrix
|
|
|
|
|
# message at most).
|
2026-10-07 23:34:59 -04:00
|
|
|
since=$(date +%s)
|
|
|
|
|
was_waiting=0
|
|
|
|
|
if [ "$state" = waiting ] && grep -q '"state":"waiting"' "$file" 2>/dev/null; then
|
|
|
|
|
was_waiting=1
|
|
|
|
|
since=$(jq -rM '.since // empty' "$file" 2>/dev/null) || since=$(date +%s)
|
|
|
|
|
[[ $since =~ ^[0-9]+$ ]] || since=$(date +%s)
|
2026-10-10 20:22:37 -04:00
|
|
|
if [ "$request" = null ]; then
|
|
|
|
|
request=$(jq -cM '.request // null' "$file" 2>/dev/null) || request=null
|
|
|
|
|
[ "$request" = null ] || message=$(jq -rM '.message // empty' "$file" 2>/dev/null)
|
|
|
|
|
fi
|
2026-10-07 23:34:59 -04:00
|
|
|
fi
|
2026-10-07 19:26:06 -04:00
|
|
|
mkdir -p "$dir" 2>/dev/null || exit 0
|
|
|
|
|
tmp=$(mktemp "$dir/.$session.XXXXXX" 2>/dev/null) || exit 0
|
2026-10-10 20:22:37 -04:00
|
|
|
if jq -cMn --arg s "$state" --arg m "$message" --arg e "$event" --argjson t "$since" --argjson r "${request:-null}" \
|
|
|
|
|
'{state: $s, since: $t, event: $e, message: (if $m == "" then null else $m end)} + (if $r then {request: $r} else {} end)' >"$tmp"; then
|
2026-10-07 19:26:06 -04:00
|
|
|
mv -f "$tmp" "$file"
|
|
|
|
|
else
|
|
|
|
|
rm -f "$tmp"
|
2026-10-07 23:34:59 -04:00
|
|
|
exit 0
|
|
|
|
|
fi
|
|
|
|
|
if [ "$state" = waiting ] && [ "$was_waiting" = 0 ]; then
|
|
|
|
|
setsid -f "${DEVDESK_LIBEXEC:-/usr/lib/devdesk}/devdesk-notify-matrix" "$session" "$since" \
|
|
|
|
|
</dev/null >/dev/null 2>&1 || true
|
2026-10-07 19:26:06 -04:00
|
|
|
fi
|
|
|
|
|
exit 0
|