🕵️ SicherheitslückenWhat continuous operational resilience looks like under DORA(09.09.2026 um 17:53 Uhr)
🔧 AI Nachrichten OpenAI seeks tougher AI rules. CIOs may feel the ripple effects(10.09.2026 um 12:11 Uhr)
🔧 AI Nachrichten Mistral valued at €21bn after €3bn Series D funding round(08.09.2026 um 10:19 Uhr)
🪟 Windows TippsWindows XP's Cursor Indicator Is Getting a Windows 11 Refresh(25.08.2026 um 13:00 Uhr)
🕵️ SicherheitslückenWhat continuous operational resilience looks like under DORA(09.09.2026 um 17:53 Uhr)
🔧 AI Nachrichten OpenAI seeks tougher AI rules. CIOs may feel the ripple effects(10.09.2026 um 12:11 Uhr)
🔧 AI Nachrichten Mistral valued at €21bn after €3bn Series D funding round(08.09.2026 um 10:19 Uhr)
🪟 Windows TippsWindows XP's Cursor Indicator Is Getting a Windows 11 Refresh(25.08.2026 um 13:00 Uhr)

🔧 Programmierung 🕛 vor 2 Monaten 9 Min Lesezeit
0

My Mac crashed with 7 Claude Code sessions open. Never again: a cross-project session picker in one bash script

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht

(brew install fzf) and python3 (ships with macOS). The tab-opening uses AppleScript for iTerm2 with a Terminal.app fallback; inside tmux it uses plain tmux new-window, which also makes that path Linux-friendly.




CODE
# save the script below, then:
chmod +x ~/bin/claude-sessions # or wherever you keep scripts on PATH
alias cs='claude-sessions' # optional









The script






CODE
#!/usr/bin/env bash
# claude-sessions — cross-project Claude Code session picker.
#
# Lists every session under ~/.claude/projects (all directories, newest first)
# in fzf. Enter opens the highlighted session in its own window running
# `claude --resume <id>` interactively (so its summary-vs-full prompt appears
# there), while the picker stays open for the next one:
# - inside tmux -> new tmux window
# - otherwise -> new TAB in the current iTerm window (Terminal.app fallback)
#
# Keys: enter=open ctrl-r=refresh esc=quit
set -euo pipefail

SELF="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
CLAUDE_BIN="${CLAUDE_BIN:-$(command -v claude || echo claude)}"
MAX_SESSIONS="${CLAUDE_SESSIONS_MAX:-300}"

# ---------------------------------------------------------------- list -----
# Emits tab-separated lines: session_id <TAB> cwd <TAB> jsonl_path <TAB> display
# $1 = "all" (default) or "main" (hide teammate/agent sessions)
list_sessions() {
python3 - "$MAX_SESSIONS" "${1:-all}" <<'PY'
import glob, json, os, re, signal, sys, time
signal.signal(signal.SIGPIPE, signal.SIG_DFL)

max_n, mode = int(sys.argv[1]), sys.argv[2]
root = os.path.expanduser("~/.claude/projects")
files = [f for f in glob.glob(os.path.join(root, "*", "*.jsonl"))
if not os.path.basename(f).startswith("agent-")]
files.sort(key=os.path.getmtime, reverse=True)

DIM, CYAN, YELLOW, MAGENTA, RESET = "
\033[2m", "\033[36m", "\033[33m", "\033[35m", "\033[0m"

def age(ts):
s = int(time.time() - ts)
if s < 3600: return f"{s // 60}m"
if s < 86400: return f"{s // 3600}h"
return f"{s // 86400}d"

def first_text(content):
if isinstance(content, str): return content
if isinstance(content, list):
for part in content:
if isinstance(part, dict) and part.get("type") == "text":
return part.get("text", "")
return ""

shown = 0
home = os.path.expanduser("~")
for f in files:
if shown >= max_n: break
sid = cwd = branch = agent = ""
title = summary = raw = ""
try:
with open(f, errors="replace") as fh:
for i, line in enumerate(fh):
if i > 40 or (title and cwd): break
try: e = json.loads(line)
except Exception: continue
if e.get("type") == "summary" and not summary:
summary = e.get("summary", "")
if e.get("type") == "user" and not e.get("isSidechain"):
sid = sid or e.get("sessionId", "")
cwd = cwd or e.get("cwd", "")
branch = branch or (e.get("gitBranch") or "")
agent = agent or (e.get("agentName") or "")
if not title:
t = first_text(e.get("message", {}).get("content", "")).strip()
if t.startswith("Caveat:"):
t = t.split("
\n", 1)[-1].strip()
raw = raw or t
if t and not t.startswith("<"):
title = t
except OSError:
continue
if not sid or not cwd:
continue
is_agent = bool(agent) and agent != "team-lead"
if mode == "main" and is_agent:
continue
if not title and raw: # e.g. teammate prompts wrapped in injected <tags>
title = re.sub(r"<[^>]*>", " ", raw)
title = " ".join((title or summary or "(no message)").split())[:120]
proj = cwd.replace(home, "~")
if len(proj) > 34: proj = "…" + proj[-33:]
br = f" {YELLOW}{branch[:14]}{RESET}" if branch and branch not in ("main", "master", "HEAD") else ""
tag = f" {MAGENTA}⛭ {agent[:18]}{RESET}" if is_agent else ""
display = (f"{DIM}{age(os.path.getmtime(f)):>3}{RESET} "
f"{CYAN}{proj:<34}{RESET}{tag}{br} {title}")
print(f"{sid}
\t{cwd}\t{f}\t{display}")
shown += 1
PY
}

# ------------------------------------------------------------- preview -----
preview_session() {
python3 - "$1" <<'PY'
import json, os, sys

path = sys.argv[1]
size = os.path.getsize(path)
with open(path, "rb") as fh:
if size > 300_000:
fh.seek(-300_000, 2)
fh.readline() # drop partial line
lines = fh.read().decode(errors="replace").splitlines()

BOLD, GREEN, BLUE, RESET = "
\033[1m", "\033[32m", "\033[34m", "\033[0m"
out = []
for line in lines:
try: e = json.loads(line)
except Exception: continue
role = e.get("type")
if role not in ("user", "assistant") or e.get("isSidechain"): continue
c = e.get("message", {}).get("content", "")
if isinstance(c, list):
c = "
\n".join(p.get("text", "") for p in c
if isinstance(p, dict) and p.get("type") == "text")
if not isinstance(c, str) or not c.strip() or c.startswith("<"): continue
tag = f"{GREEN}{BOLD}user{RESET}" if role == "user" else f"{BLUE}{BOLD}claude{RESET}"
out.append(f"{tag}: {c.strip()[:600]}")
print(f"
\n\n".join(out[-15:]) or "(no readable messages)")
PY
}

# ---------------------------------------------------------------- open -----
open_one() {
local id="$1" cwd="$2"
[ -d "$cwd" ] || cwd="$HOME"
if [ -n "${TMUX:-}" ]; then
tmux new-window -c "$cwd" -n "$(basename "$cwd")" "$CLAUDE_BIN --resume $id"
elif [ "${TERM_PROGRAM:-}" = "iTerm.app" ] || ps -axo comm | grep -q "MacOS/iTerm2$"; then
osascript - "$cwd" "$id" "$CLAUDE_BIN" <<'EOF' >/dev/null
on run argv
set theCmd to "cd " & quoted form of item 1 of argv & " && " & item 3 of argv & " --resume " & item 2 of argv
tell application "iTerm"
activate
if (count of windows) is 0 then
create window with default profile
else
tell current window to create tab with default profile
end if
tell current session of current window to write text theCmd
end tell
end run
EOF
else
osascript - "$cwd" "$id" "$CLAUDE_BIN" <<'EOF' >/dev/null
on run argv
set theCmd to "cd " & quoted form of item 1 of argv & " && " & item 3 of argv & " --resume " & item 2 of argv
tell application "Terminal"
activate
do script theCmd
end tell
end run
EOF
fi
}

open_line() { # $1 = file containing the highlighted tab-delimited line
IFS=$'\t' read -r id cwd _rest < "$1"
[ -n "$id" ] && open_one "$id" "$cwd"
}

# ---------------------------------------------------------------- main -----
case "${1:-}" in
--list) list_sessions "${2:-all}" ;;
--preview) preview_session "$2" ;;
--open-line) open_line "$2" ;;
*)
list_sessions | fzf --ansi --delimiter '\t' --with-nth 4.. \
--no-sort --layout=reverse --info=inline \
--header 'enter: open in new tab (picker stays) | ctrl-a: hide/show ⛭ agents | ctrl-r: refresh | esc: quit' \
--preview "'$SELF' --preview {3}" --preview-window 'right,50%,wrap' \
--bind "enter:execute-silent('$SELF' --open-line {f})" \
--bind "ctrl-r:reload('$SELF' --list all)" \
--bind "ctrl-a:transform:[ \"\$FZF_PROMPT\" = 'main> ' ] \
&& echo
\"change-prompt(> )+reload('$SELF' --list all)\" \
|| echo
\"change-prompt(main> )+reload('$SELF' --list main)\"" \
>/dev/null || true
;;
esac









Caveats




  • The ~/.claude/projects JSONL layout is undocumented internal storage — a Claude Code update could change it. The script only reads these files, so the worst case is the picker breaks, never your sessions.

  • Window/tab spawning is macOS-specific (AppleScript). On Linux, run it inside tmux and it just works.

  • The list is capped at the 300 most recent sessions (CLAUDE_SESSIONS_MAX to change).






What I'd add next



A Homebrew tap so it's brew install-able, and maybe ctrl-d to archive dead sessions. But honestly, the core lesson is the fun part: when a CLI tool stores its state as plain files, you're one fzf away from the UI you wish it had.



Fittingly, I built this with Claude Code itself — it read its own session format off disk and wrote its own recovery tool in about 20 minutes.

Vollständiger Original-Bericht
Ausführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
1 Quelle
Sam Altman calls GPT-6 Astra rollout ‘messy’ as enterprise users wait for access
1 Quelle
Swiss government explores replacing Microsoft 365 with open-source software
1 Quelle
What continuous operational resilience looks like under DORA
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten My Mac crashed with 7 Claude Code sessions open. Never again: a cross-project session picker in one bash script

Thematisch verwandte Begriffe: crashed, with, Claude, Code · 6 Treffer

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...