Skip to content

Instantly share code, notes, and snippets.

@mohitmun
Created April 29, 2026 10:58
Show Gist options
  • Select an option

  • Save mohitmun/40d0509930900c068d553cfc874f72e4 to your computer and use it in GitHub Desktop.

Select an option

Save mohitmun/40d0509930900c068d553cfc874f72e4 to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
# install.sh — set up the Claude Code bookmark feature.
#
# What it installs:
# ~/.claude/scripts/bm/save.sh called by the /bookmark slash command
# ~/.claude/scripts/bm/count.sh used by the shell prompt
# ~/.claude/commands/bookmark.md the /bookmark slash command
# ~/.claude/bookmarks.json storage file (created if missing)
# a managed block in ~/.zshrc defines the `bm` function and adds
# "(N bookmarks)" to RPROMPT
#
# Re-running is safe — the .zshrc block is replaced between marker comments.
#
# Requires: jq, claude (Claude Code CLI), zsh.
set -euo pipefail
CLAUDE_DIR="${CLAUDE_DIR:-$HOME/.claude}"
ZSHRC="${ZSHRC:-$HOME/.zshrc}"
echo "Installing Claude Code bookmarks..."
if ! command -v jq >/dev/null 2>&1; then
echo "ERROR: jq is required."
echo " macOS: brew install jq"
echo " Linux: apt install jq (or your distro's equivalent)"
exit 1
fi
if ! command -v claude >/dev/null 2>&1; then
echo "WARNING: 'claude' CLI not found on PATH — bookmarks can be saved but 'bm <id>' won't resume sessions."
fi
mkdir -p "$CLAUDE_DIR/scripts/bm" "$CLAUDE_DIR/commands"
# ---------- save.sh ----------
cat > "$CLAUDE_DIR/scripts/bm/save.sh" <<'BMSAVE'
#!/usr/bin/env bash
# Save the current Claude Code session as a bookmark.
set -euo pipefail
NAME="${1:-}"
NAME="${NAME#\"}"; NAME="${NAME%\"}"
NAME="${NAME#\'}"; NAME="${NAME%\'}"
if [ -z "$NAME" ]; then
echo "usage: save.sh <bookmark-name>" >&2
exit 1
fi
CWD="$(pwd)"
ENCODED="$(printf '%s' "$CWD" | tr '/' '-')"
SESSIONS_DIR="$HOME/.claude/projects/$ENCODED"
if [ ! -d "$SESSIONS_DIR" ]; then
echo "No Claude session directory at $SESSIONS_DIR" >&2
exit 1
fi
SESSION_FILE="$(ls -t "$SESSIONS_DIR"/*.jsonl 2>/dev/null | head -1 || true)"
if [ -z "$SESSION_FILE" ]; then
echo "No session .jsonl found in $SESSIONS_DIR" >&2
exit 1
fi
SESSION_ID="$(basename "$SESSION_FILE" .jsonl)"
BRANCH="$(git -C "$CWD" symbolic-ref --short HEAD 2>/dev/null || echo "")"
CREATED="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
BOOKMARKS="$HOME/.claude/bookmarks.json"
[ -f "$BOOKMARKS" ] || echo '[]' > "$BOOKMARKS"
if jq -e --arg sid "$SESSION_ID" 'any(.[]; .session_id == $sid)' "$BOOKMARKS" >/dev/null; then
EXISTING=$(jq -r --arg sid "$SESSION_ID" '.[] | select(.session_id == $sid) | "[\(.id)] \(.name)"' "$BOOKMARKS")
echo "This session is already bookmarked as $EXISTING"
exit 0
fi
NEXT=$(jq '[.[].id | tonumber] | (if length == 0 then 0 else max end) + 1' "$BOOKMARKS")
ID=$(printf "%03d" "$NEXT")
TMP="$(mktemp)"
jq --arg id "$ID" --arg name "$NAME" --arg sid "$SESSION_ID" \
--arg path "$CWD" --arg branch "$BRANCH" --arg created "$CREATED" \
'. += [{id: $id, name: $name, session_id: $sid, project_path: $path, branch: $branch, created_at: $created}]' \
"$BOOKMARKS" > "$TMP" && mv "$TMP" "$BOOKMARKS"
echo "Bookmarked [$ID] '$NAME' -> session ${SESSION_ID:0:8}... (branch: ${BRANCH:-none})"
BMSAVE
chmod +x "$CLAUDE_DIR/scripts/bm/save.sh"
# ---------- count.sh ----------
cat > "$CLAUDE_DIR/scripts/bm/count.sh" <<'BMCOUNT'
#!/usr/bin/env bash
# Print "(N bookmarks)" — total across all projects. Empty if zero.
BOOKMARKS="$HOME/.claude/bookmarks.json"
[ -s "$BOOKMARKS" ] || exit 0
COUNT=$(jq -r 'length' "$BOOKMARKS" 2>/dev/null || echo 0)
if [ "${COUNT:-0}" -gt 0 ]; then
if [ "$COUNT" -eq 1 ]; then
printf '(1 bookmark)'
else
printf '(%d bookmarks)' "$COUNT"
fi
fi
BMCOUNT
chmod +x "$CLAUDE_DIR/scripts/bm/count.sh"
# ---------- slash command ----------
cat > "$CLAUDE_DIR/commands/bookmark.md" <<'BMCMD'
---
description: Bookmark this Claude session so you can resume it later via `bm`
argument-hint: <bookmark name>
allowed-tools: [Bash]
---
Run this exact command via Bash and report only its output verbatim — no commentary, no summary:
```
~/.claude/scripts/bm/save.sh "$ARGUMENTS"
```
BMCMD
# ---------- bookmarks.json ----------
[ -f "$CLAUDE_DIR/bookmarks.json" ] || echo '[]' > "$CLAUDE_DIR/bookmarks.json"
# ---------- ~/.zshrc managed block ----------
ZSH_BLOCK='# >>> claude bookmarks (managed) >>>
setopt PROMPT_SUBST 2>/dev/null
bm_prompt() {
local out=$(~/.claude/scripts/bm/count.sh 2>/dev/null)
[[ -n "$out" ]] && echo "%F{yellow}${out}%f"
}
bm() {
local file="$HOME/.claude/bookmarks.json"
[[ -f "$file" ]] || { echo "No bookmarks yet. Use /bookmark <name> inside a Claude session."; return 0; }
local cmd="${1:-list}"
case "$cmd" in
list|ls|all|-a|--all|"")
local rows=$(jq -r '"'"'.[] | "\(.id)\t\(.name)\t\(.branch)\t\(.project_path)"'"'"' "$file")
if [[ -z "$rows" ]]; then
echo "No bookmarks yet. Use /bookmark <name> inside a Claude session."
else
printf "%s\n" "$rows" | column -ts $'"'"'\t'"'"'
fi
;;
rm|delete)
local id="${2:?usage: bm rm <id>}"
local match=$(jq -r --arg id "$id" '"'"'.[] | select(.id == $id) | .name'"'"' "$file")
[[ -z "$match" ]] && { echo "Bookmark $id not found." >&2; return 1; }
local tmp=$(mktemp)
jq --arg id "$id" '"'"'map(select(.id != $id))'"'"' "$file" > "$tmp" && mv "$tmp" "$file"
echo "Removed [$id] $match"
;;
*)
local id="$cmd"
local sid=$(jq -r --arg id "$id" '"'"'.[] | select(.id == $id) | .session_id'"'"' "$file")
local proj_dir=$(jq -r --arg id "$id" '"'"'.[] | select(.id == $id) | .project_path'"'"' "$file")
if [[ -z "$sid" ]]; then
echo "Bookmark $id not found. Run '"'"'bm'"'"' to list." >&2
return 1
fi
cd "$proj_dir" && claude --resume "$sid"
;;
esac
}
# Append bookmark count to RPROMPT (right-side prompt). Safe for any left prompt.
RPROMPT="${RPROMPT:+${RPROMPT} }"'"'"'$(bm_prompt)'"'"'
# <<< claude bookmarks (managed) <<<'
touch "$ZSHRC"
TMP_RC="$(mktemp)"
awk '
/^# >>> claude bookmarks \(managed\) >>>/ { skip=1; next }
/^# <<< claude bookmarks \(managed\) <<</ { skip=0; next }
!skip { print }
' "$ZSHRC" > "$TMP_RC"
{
cat "$TMP_RC"
printf '\n%s\n' "$ZSH_BLOCK"
} > "$ZSHRC.new"
mv "$ZSHRC.new" "$ZSHRC"
rm -f "$TMP_RC"
echo
echo "Installed:"
echo " ~/.claude/scripts/bm/save.sh"
echo " ~/.claude/scripts/bm/count.sh"
echo " ~/.claude/commands/bookmark.md"
echo " ~/.claude/bookmarks.json"
echo " managed block appended to $ZSHRC"
echo
echo "Activate: source $ZSHRC"
echo
echo "Usage:"
echo " /bookmark <name> inside a Claude session — save it"
echo " bm list all bookmarks"
echo " bm <id> cd to its project and resume that session"
echo " bm rm <id> delete a bookmark"
echo
echo "By default the count shows on the right side of the prompt. To put it"
echo "inline with your left prompt instead, edit $ZSHRC and replace the"
echo "RPROMPT line with: PROMPT=\"\$PROMPT \\\$(bm_prompt) \""
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment