#!/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 # PermissionRequest -> waiting, with what Claude wants to run # (tool + command/file/URL), for the inbox's # Allow/Deny (devdesk#22) # 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. # Entering "waiting" also starts devdesk-notify-matrix, which posts to Matrix if # the wait lasts and nobody is attached (devdesk#28). 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 # Unit separator, not tab: read collapses runs of whitespace delimiters, so an # empty field would shift the ones after it. IFS=$'\x1f' read -r event message tool file < <(jq -rM '[.hook_event_name // "", (.message // "" | gsub("[\u001f\n]"; " ")), .tool_name // "", (.tool_input.file_path // .tool_input.notebook_path // "" | gsub("[\u001f\n]"; " "))] | join("\u001f")' <<<"$input" 2>/dev/null) # 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 case $event in UserPromptSubmit | PostToolUse) state=working ;; Notification | PermissionRequest) state=waiting ;; 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 # 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 # A second Notification during the same wait updates the message but keeps # `since` (and what the wait is about), so the wait stays one wait (one Matrix # message at most). 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) 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 fi mkdir -p "$dir" 2>/dev/null || exit 0 tmp=$(mktemp "$dir/.$session.XXXXXX" 2>/dev/null) || exit 0 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 mv -f "$tmp" "$file" else rm -f "$tmp" exit 0 fi if [ "$state" = waiting ] && [ "$was_waiting" = 0 ]; then setsid -f "${DEVDESK_LIBEXEC:-/usr/lib/devdesk}/devdesk-notify-matrix" "$session" "$since" \ /dev/null 2>&1 || true fi exit 0