Skip to content

Instantly share code, notes, and snippets.

@ericboehs
Created January 28, 2026 14:59
Show Gist options
  • Select an option

  • Save ericboehs/8586c0e7449015fb7a6ea374d4ae5018 to your computer and use it in GitHub Desktop.

Select an option

Save ericboehs/8586c0e7449015fb7a6ea374d4ae5018 to your computer and use it in GitHub Desktop.
Claude Code session inspection tools - list and tail active sessions

Claude Code Session Tools

Two shell scripts for inspecting active Claude Code sessions.

Installation

# Download both scripts
curl -sL https://gist.github.com/ericboehs/8586c0e7449015fb7a6ea374d4ae5018/raw/claude-active -o ~/.local/bin/claude-active
curl -sL https://gist.github.com/ericboehs/8586c0e7449015fb7a6ea374d4ae5018/raw/claude-tail -o ~/.local/bin/claude-tail
chmod +x ~/.local/bin/claude-{active,tail}

claude-active

List active Claude Code sessions from the last N minutes.

claude-active              # Interactive fzf picker (last 5 min)
claude-active 60           # Sessions from last hour
claude-active --no-fzf     # List to stdout

claude-tail

Tail a Claude Code session (like tail -f for conversations).

claude-tail <uuid>           # Follow session output
claude-tail <uuid> --no-follow  # One-shot output
claude-tail <uuid> -t        # Include tool calls
claude-tail <uuid> -T        # Only show tool calls
claude-tail <uuid> -o        # Show tool output/results
claude-tail <uuid> --glow    # Render markdown with glow

Workflow

# Pick a session interactively, then tail it
claude-active | claude-tail

# Or specify partial UUID directly
claude-tail c72bd55a

Dependencies

  • jq - JSON processing
  • fzf (optional) - interactive session picker
  • glow (optional) - markdown rendering
#!/bin/bash
# claude-active - List active Claude Code sessions
# Usage: claude-active [options] [minutes]
# minutes: how far back to look (default: 5)
# --no-fzf: output to stdout instead of fzf picker
#
# If fzf is available and terminal is interactive, opens picker
# Selected session UUID is output for piping to claude-tail
NO_FZF=false
minutes=5
while [[ $# -gt 0 ]]; do
case $1 in
--no-fzf)
NO_FZF=true
shift
;;
-h|--help)
echo "Usage: claude-active [options] [minutes]"
echo ""
echo "Options:"
echo " --no-fzf Output to stdout instead of fzf picker"
echo " -h, --help Show this help"
echo ""
echo "Arguments:"
echo " minutes How far back to look (default: 5)"
echo ""
echo "Examples:"
echo " claude-active # Interactive picker (last 5 min)"
echo " claude-active 60 # Sessions from last hour"
echo " claude-active --no-fzf # List to stdout"
echo " claude-active | claude-tail # Pipe selected to tail"
exit 0
;;
*)
if [[ "$1" =~ ^[0-9]+$ ]]; then
minutes="$1"
fi
shift
;;
esac
done
get_sessions() {
find ~/.claude/projects -name "*.jsonl" -mmin -"$minutes" 2>/dev/null |
grep -v '/subagents/' |
while read f; do
uuid=$(basename "$f" .jsonl)
project=$(dirname "$f" | xargs basename | sed 's/^-//; s/-/\//g')
mtime=$(stat -f "%Sm" -t "%H:%M" "$f")
first_msg=$(jq -r 'select(.type == "user" and (.message.content | type) == "string") | .message.content' "$f" 2>/dev/null | head -1 | cut -c1-50 | tr '\n' ' ')
printf "%s | %s | %-12s | %s\n" "$mtime" "$uuid" "$(basename "$project")" "$first_msg"
done | sort -r
}
sessions=$(get_sessions)
if [[ -z "$sessions" ]]; then
echo "No active sessions found in the last $minutes minutes" >&2
exit 1
fi
# Use fzf if available, interactive (stdin is tty), and not disabled
# fzf uses /dev/tty for interaction, so it works even when stdout is piped
if [[ "$NO_FZF" == false ]] && command -v fzf &>/dev/null && [[ -t 0 ]]; then
selected=$(echo "$sessions" | fzf \
--prompt="Select session: " \
--height=40% \
--border \
--header="Active Claude sessions (last ${minutes}m)" \
--preview="uuid=\$(echo {} | awk -F'|' '{print \$2}' | tr -d ' '); find ~/.claude/projects -name \"\${uuid}.jsonl\" 2>/dev/null | grep -v subagents | head -1 | xargs -I{} jq -r 'select(.type == \"assistant\") | .message.content[] | select(.type == \"text\") | .text' {} 2>/dev/null | tail -30" \
--preview-window=right:50%:wrap)
if [[ -n "$selected" ]]; then
# Output just the UUID
echo "$selected" | awk -F'|' '{print $2}' | tr -d ' '
fi
else
echo "$sessions"
fi
#!/bin/bash
# claude-tail - Show assistant messages from a Claude Code session
# Usage: claude-tail [uuid] [options]
# uuid: session UUID (or partial match), can also be piped via stdin
#
# By default, follows the session (like tail -f). Use Ctrl-C to stop.
uuid=""
follow=true
lines=""
show_tools=false
only_tools=false
show_tools_output=false
use_glow=false
show_timestamps=false
while [[ $# -gt 0 ]]; do
case $1 in
-f|--follow)
follow=true
shift
;;
--no-follow)
follow=false
shift
;;
-n|--lines)
lines="$2"
shift 2
;;
-t|--show-tools)
show_tools=true
shift
;;
-T|--only-tools)
only_tools=true
show_tools=true
shift
;;
-o|--show-tools-output)
show_tools_output=true
shift
;;
--glow)
use_glow=true
shift
;;
--show-timestamps)
show_timestamps=true
shift
;;
-h|--help)
echo "Usage: claude-tail [uuid] [options]"
echo ""
echo "Options:"
echo " -f, --follow Follow output (default)"
echo " --no-follow Don't follow, just output once"
echo " -n, --lines N Only show last N lines"
echo " -t, --show-tools Show tool/command calls"
echo " -T, --only-tools Only show tool calls (no text)"
echo " -o, --show-tools-output Show tool output/results"
echo " --glow Render output through glow (markdown)"
echo " --show-timestamps Show 12-hour time before messages"
echo " -h, --help Show this help"
echo ""
echo "Examples:"
echo " claude-tail c72bd55a # Follow session (partial UUID)"
echo " claude-tail --no-follow # One-shot output"
echo " claude-active | claude-tail # Pick session then tail"
echo " claude-tail c72bd55a -n 20 # Last 20 lines, then follow"
echo " claude-tail c72bd55a -t # Include tool calls"
echo " claude-tail c72bd55a -T # Only tool calls"
exit 0
;;
*)
if [[ -z "$uuid" ]]; then
uuid="$1"
fi
shift
;;
esac
done
# Read UUID from stdin if not provided and stdin is not a terminal
if [[ -z "$uuid" ]] && [[ ! -t 0 ]]; then
read -r input
# Extract UUID from piped input (handles both raw UUID and formatted line)
# Format: "11:28 | c9ef7a33-... | cli | message"
if [[ "$input" == *"|"* ]]; then
uuid=$(echo "$input" | awk -F'|' '{print $2}' | tr -d ' ')
else
uuid=$(echo "$input" | tr -d ' ')
fi
fi
if [[ -z "$uuid" ]]; then
echo "Usage: claude-tail <uuid> [options]"
echo ""
echo "Find active sessions with: claude-active"
exit 1
fi
# Find session file (supports partial UUID match)
find_session() {
find ~/.claude/projects -name "${uuid}*.jsonl" 2>/dev/null | grep -v subagents | head -1
}
session=$(find_session)
if [[ -z "$session" ]]; then
echo "Session not found: $uuid" >&2
exit 1
fi
echo "Tailing session: $(basename "$session" .jsonl)" >&2
echo "Press Ctrl-C to stop" >&2
echo "---" >&2
# Function to extract assistant text (with blank line between messages)
extract_messages() {
local sep=$'\n'
local username=$(whoami)
local time_fmt='
def format_time:
if . then
(sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601 | localtime) as $t |
($t[3]) as $h |
($t[4] | tostring | if length == 1 then "0" + . else . end) as $m |
(if $h == 0 then 12 elif $h > 12 then $h - 12 else $h end) as $h12 |
"\($h12):\($m) "
else "" end;
'
local tool_fmt='
"→ " + .name + (
if .name == "Bash" then ":\n " + (.input.command // "" | gsub("\n"; "\n "))
elif .name == "Read" then ": " + (.input.file_path // "")
elif .name == "Edit" then ": " + (.input.file_path // "")
elif .name == "Write" then ": " + (.input.file_path // "")
elif .name == "Grep" then "(" + (["pattern: \(.input.pattern // "" | tojson)", (if .input.path then "path: \(.input.path | tojson)" else null end), (if .input.output_mode then "output_mode: \(.input.output_mode | tojson)" else null end)] | map(select(. != null)) | join(", ")) + ")"
elif .name == "Glob" then "(" + (["pattern: \(.input.pattern // "" | tojson)", (if .input.path then "path: \(.input.path | tojson)" else null end)] | map(select(. != null)) | join(", ")) + ")"
elif .name == "Task" then ": " + (.input.description // "") + " [" + (.input.subagent_type // "agent") + "]"
elif .name == "WebSearch" then "(" + (.input.query // "" | tojson) + ")"
elif .name == "WebFetch" then ": " + (.input.url // "")
elif .name == "LSP" then "(" + (.input.operation // "") + " at " + (.input.filePath // "") + ":" + ((.input.line // 0) | tostring) + ")"
elif .name == "AskUserQuestion" then ""
else ": " + (.input | tostring | .[0:100])
end
)
'
local result_fmt='" ⎿ " + (if (.content | type) == "string" then .content else (.content[]? | select(.type == "text") | .text) // "" end | gsub("\n"; "\n "))'
if [[ "$show_tools_output" == true ]] && [[ "$only_tools" == true ]]; then
# Only tools + their output
jq -r "
if .type == \"assistant\" then
.message.content[] | if .type == \"tool_use\" then $tool_fmt else empty end
elif .type == \"user\" then
.message.content[] | if .type == \"tool_result\" then $result_fmt else empty end
else empty end
" "$session" 2>/dev/null
elif [[ "$show_tools_output" == true ]] && [[ "$show_tools" == true ]]; then
# Text + tools + output
jq -r --arg sep "$sep" "
if .type == \"assistant\" then
.message.content[] |
if .type == \"text\" then .text
elif .type == \"tool_use\" then $tool_fmt
else empty end
elif .type == \"user\" then
.message.content[] | if .type == \"tool_result\" then $result_fmt else empty end
else empty end
" "$session" 2>/dev/null
elif [[ "$show_tools_output" == true ]]; then
# Only tool output (no tool calls shown)
jq -r --arg sep "$sep" '
select(.type == "user") | .message.content[] |
if .type == "tool_result" then
if (.content | type) == "string" then .content
else (.content[]? | select(.type == "text") | .text) // "" end
else empty end
' "$session" 2>/dev/null
elif [[ "$only_tools" == true ]]; then
jq -r "select(.type == \"assistant\") | .message.content[] |
if .type == \"tool_use\" then $tool_fmt
else empty end" "$session" 2>/dev/null
elif [[ "$show_tools" == true ]]; then
local ts_prefix=""
local ts_prefix_asst=""
if [[ "$show_timestamps" == true ]]; then
ts_prefix="(.timestamp | format_time) + "
ts_prefix_asst="(\$ts | format_time) + "
fi
jq -r --arg sep "$sep" --arg user "$username" "
$time_fmt
if .type == \"user\" then
if (.message.content | type) == \"string\" then
(if \$sep != \"\" then \$sep + \"\n\n\" else \"\" end) + ${ts_prefix}\"\" + .message.content
elif (.message.content | map(select(.type == \"text\")) | length) > 0 then
(if \$sep != \"\" then \$sep + \"\n\n\" else \"\" end) + ${ts_prefix}\"\" + (.message.content[] | select(.type == \"text\") | .text)
else empty end
elif .type == \"assistant\" then
.timestamp as \$ts |
.message.content[] |
if .type == \"text\" then (if \$sep != \"\" then \$sep + \"\n\n\" else \"\" end) + ${ts_prefix_asst}.text
elif .type == \"tool_use\" then $tool_fmt
else empty end
else empty end" "$session" 2>/dev/null
else
local ts_prefix=""
local ts_prefix_asst=""
if [[ "$show_timestamps" == true ]]; then
ts_prefix="(.timestamp | format_time) + "
ts_prefix_asst="(\$ts | format_time) + "
fi
jq -r --arg sep "$sep" --arg user "$username" "
$time_fmt
if .type == \"user\" then
if (.message.content | type) == \"string\" then
(if \$sep != \"\" then \$sep + \"\n\n\" else \"\" end) + ${ts_prefix}\"\" + .message.content
elif (.message.content | map(select(.type == \"text\")) | length) > 0 then
(if \$sep != \"\" then \$sep + \"\n\n\" else \"\" end) + ${ts_prefix}\"\" + (.message.content[] | select(.type == \"text\") | .text)
else empty end
elif .type == \"assistant\" then
.timestamp as \$ts |
.message.content[] | select(.type == \"text\") |
(if \$sep != \"\" then \$sep + \"\n\n\" else \"\" end) + ${ts_prefix_asst}.text
else empty end
" "$session" 2>/dev/null
fi
}
if [[ "$use_glow" == true ]] && ! command -v glow &>/dev/null; then
echo "Error: glow is not installed. Install with: brew install glow" >&2
exit 1
fi
if [[ "$follow" == true ]]; then
# Track what we've already shown
last_count=0
while true; do
current=$(extract_messages)
current_count=$(echo "$current" | wc -l)
if [[ "$current_count" -gt "$last_count" ]]; then
if [[ "$use_glow" == true ]]; then
# Clear screen and re-render full output through glow
clear
if [[ -n "$lines" ]]; then
echo "$current" | tail -"$lines" | cat -s | glow -w 0 -
else
echo "$current" | cat -s | glow -w 0 -
fi
else
if [[ "$last_count" -eq 0 ]]; then
if [[ -n "$lines" ]]; then
echo "$current" | tail -"$lines"
else
echo "$current"
fi
else
# Show only new lines
echo "$current" | tail -n +$((last_count + 1))
fi
fi
last_count=$current_count
fi
sleep 1
done
else
output=$(extract_messages)
if [[ -n "$lines" ]]; then
if [[ "$use_glow" == true ]]; then
echo "$output" | tail -"$lines" | cat -s | glow -w 0 -
else
echo "$output" | tail -"$lines"
fi
else
if [[ "$use_glow" == true ]]; then
echo "$output" | cat -s | glow -w 0 -
else
echo "$output"
fi
fi
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment