Created
April 29, 2026 10:58
-
-
Save mohitmun/40d0509930900c068d553cfc874f72e4 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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