Last active
March 20, 2026 21:36
-
-
Save andrewvaughan/40f267e3161c5a7c10f3efec4d02bcdf to your computer and use it in GitHub Desktop.
Claude Code macOS notifications via hooks (permission prompts, questions, and completion)
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
| #!/bin/bash | |
| set -euo pipefail | |
| INPUT="$(cat)" | |
| # Skip if this is a subagent stopping, not the main agent | |
| AGENT_ID="$(echo "$INPUT" | jq -r '.agent_id // empty')" | |
| if [[ -n "$AGENT_ID" ]]; then | |
| exit 0 | |
| fi | |
| CWD="$(echo "$INPUT" | jq -r '.cwd // ""')" | |
| PROJECT="$(basename "$CWD")" | |
| terminal-notifier \ | |
| -title "Claude Code — $PROJECT" \ | |
| -message "Done" \ | |
| -sound Glass >/dev/null 2>&1 |
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
| #!/bin/bash | |
| set -eo pipefail | |
| INPUT="$(cat)" | |
| CWD="$(echo "$INPUT" | jq -r '.cwd // ""')" | |
| PROJECT="$(basename "$CWD")" | |
| terminal-notifier \ | |
| -title "Claude Code — $PROJECT" \ | |
| -message "Claude is asking a question" \ | |
| -sound Glass >/dev/null 2>&1 |
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
| #!/bin/bash | |
| set -eo pipefail | |
| INPUT="$(cat)" | |
| CWD="$(echo "$INPUT" | jq -r '.cwd // ""')" | |
| PROJECT="$(basename "$CWD")" | |
| MESSAGE="$(echo "$INPUT" | jq -r '.message // "Waiting for permission"')" | |
| terminal-notifier \ | |
| -title "Claude Code — $PROJECT" \ | |
| -message "$MESSAGE" \ | |
| -sound Glass >/dev/null 2>&1 |
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
| { | |
| "permissions": { | |
| "allow": [] | |
| }, | |
| "hooks": { | |
| "PreToolUse": [ | |
| { | |
| "matcher": "AskUserQuestion", | |
| "hooks": [ | |
| { | |
| "type": "command", | |
| "command": "~/.claude/hooks/notify-needs-attention.sh" | |
| } | |
| ] | |
| } | |
| ], | |
| "Notification": [ | |
| { | |
| "matcher": "permission_prompt", | |
| "hooks": [ | |
| { | |
| "type": "command", | |
| "command": "~/.claude/hooks/notify-permission.sh" | |
| } | |
| ] | |
| } | |
| ], | |
| "PermissionRequest": [ | |
| { | |
| "matcher": "", | |
| "hooks": [ | |
| { | |
| "type": "command", | |
| "command": "~/.claude/hooks/notify-permission.sh" | |
| } | |
| ] | |
| } | |
| ], | |
| "Stop": [ | |
| { | |
| "matcher": "", | |
| "hooks": [ | |
| { | |
| "type": "command", | |
| "command": "~/.claude/hooks/notify-done.sh" | |
| } | |
| ] | |
| } | |
| ] | |
| } | |
| } |
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 | |
| # ============================================================================= | |
| # Claude Code Status Line | |
| # ============================================================================= | |
| # | |
| # A custom status line script for Claude Code that displays project context, | |
| # GitHub integration, usage metrics, and account info in a color-coded bar. | |
| # | |
| # DISPLAY ORDER: | |
| # Repo │ Build # │ PR # (issues) │ Issue # │ Ctx │ Session │ 5h │ 7d │ Org | |
| # | |
| # SETUP: | |
| # 1. Save this file somewhere (e.g., ~/.claude/statusline-command.sh) | |
| # 2. Make it executable: chmod +x ~/.claude/statusline-command.sh | |
| # 3. Add to ~/.claude/settings.json: | |
| # { | |
| # "statusLine": { | |
| # "type": "command", | |
| # "command": "~/.claude/statusline-command.sh" | |
| # } | |
| # } | |
| # | |
| # DEPENDENCIES: | |
| # - jq (required) — parses the JSON that Claude Code pipes to stdin | |
| # - git (optional) — repo name, branch detection, GitHub URL construction | |
| # - gh (optional) — PR lookup and linked issue detection (GitHub CLI) | |
| # - claude (optional) — org/subscription info via `claude auth status` | |
| # | |
| # HOW EACH SECTION WORKS: | |
| # | |
| # Repo Git remote origin name. Requires: git remote configured. | |
| # | |
| # Build # Extracted from the working directory path if it contains | |
| # "build-##" (e.g., /path/build-08/repo → "Build #08"). | |
| # Useful when running multiple parallel clones. | |
| # | |
| # PR # Looks up an open PR for the current branch via `gh pr view`. | |
| # Parses the PR body for "Closes/Fixes/Resolves #N" references | |
| # and shows them as clickable links. Cached for 10 minutes. | |
| # Skill workflows clear the cache on PR/branch state changes. | |
| # Requires: gh CLI, authenticated with GitHub. | |
| # | |
| # Issue # Parsed from the branch name using the convention: | |
| # <type>/<issue#>-<slug> (e.g., feature/123-user-auth → Issue #123). | |
| # Valid types: feature, fix, docs, refactor, test, chore, security, | |
| # hotfix, bug, task, issue, feat. Skipped if already shown in PR. | |
| # Requires: branch follows the naming convention. | |
| # | |
| # Ctx Context window usage percentage + window size (e.g., "63% 200k"). | |
| # Color-coded: green <40%, yellow 40-59%, orange 60-79%, red 80%+. | |
| # Source: Claude Code JSON (always available). | |
| # | |
| # Session Cumulative API cost for the current session (e.g., "$1.09"). | |
| # Source: Claude Code JSON (always available). | |
| # | |
| # 5h / 7d Rate limit usage for the 5-hour and 7-day windows, with time | |
| # until reset. Color-coded like Ctx. Only shown for Pro/Max plans. | |
| # Source: Claude Code JSON (available after first API response). | |
| # | |
| # Org Anthropic organization name and subscription tier from | |
| # `claude auth status`. Strips trailing "Organization" from the | |
| # name. Cached for 5 minutes. Requires: claude CLI, logged in. | |
| # | |
| # CACHING: | |
| # - PR lookup: /tmp/claude-statusline-pr-<hash> (10min TTL, cleared by skill workflows) | |
| # - Org info: /tmp/claude-statusline-org (5min TTL) | |
| # To force refresh, delete the cache files. | |
| # | |
| # CLICKABLE LINKS: | |
| # PR numbers and issue numbers are rendered as OSC 8 hyperlinks pointing to | |
| # their GitHub URLs. Requires a terminal that supports OSC 8 (most modern | |
| # terminals: iTerm2, Wezterm, Windows Terminal, GNOME Terminal 3.26+, etc.). | |
| # | |
| # COLOR SCHEME: | |
| # Repo name .... bold cyan Rate limit labels .. dim | |
| # Build # ...... bright blue Percentages ........ green/yellow/orange/red | |
| # PR # ......... magenta Reset times ........ dim | |
| # Issue # ...... yellow Org name ........... cyan | |
| # Labels ....... dim Subscription ....... dim | |
| # Separator .... dim │ | |
| # | |
| # ============================================================================= | |
| input=$(cat) | |
| # ANSI colors | |
| DIM='\033[2m' | |
| BOLD='\033[1m' | |
| RESET='\033[0m' | |
| CYAN='\033[36m' | |
| BLUE='\033[94m' | |
| ORANGE='\033[38;5;208m' | |
| MAGENTA='\033[35m' | |
| GREEN='\033[32m' | |
| YELLOW='\033[33m' | |
| RED='\033[31m' | |
| SEP="${DIM} │ ${RESET}" | |
| # Color a percentage: green <40, yellow 40-59, orange 60-79, red 80+ | |
| pct_color() { | |
| local val=$1 | |
| if [ "$val" -ge 80 ]; then echo -n "${RED}" | |
| elif [ "$val" -ge 60 ]; then echo -n "${ORANGE}" | |
| elif [ "$val" -ge 40 ]; then echo -n "${YELLOW}" | |
| else echo -n "${GREEN}" | |
| fi | |
| } | |
| parts=() | |
| cwd=$(echo "$input" | jq -r '.cwd // empty') | |
| # --- Repo name and base URL for links --- | |
| gh_base="" | |
| if [ -n "$cwd" ] && command -v git >/dev/null 2>&1; then | |
| repo_url=$(git -C "$cwd" --no-optional-locks remote get-url origin 2>/dev/null) | |
| if [ -n "$repo_url" ]; then | |
| repo_name=$(basename -s .git "$repo_url") | |
| # Build GitHub base URL from remote (handles ssh and https) | |
| gh_base=$(echo "$repo_url" | sed -E 's|git@github\.com:|https://github.com/|; s|\.git$||') | |
| parts+=("${BOLD}${CYAN}${repo_name}${RESET}") | |
| fi | |
| fi | |
| # OSC 8 hyperlink helper: link URL TEXT | |
| link() { | |
| printf '\033]8;;%s\a%s\033]8;;\a' "$1" "$2" | |
| } | |
| # --- Build number from cwd --- | |
| build=$(echo "$cwd" | grep -oE 'build-[0-9]+' | head -1) | |
| if [ -n "$build" ]; then | |
| build_num=$(echo "$build" | grep -oE '[0-9]+') | |
| parts+=("${BLUE}Build #${build_num}${RESET}") | |
| fi | |
| # --- Get branch once for PR and Issue sections --- | |
| branch="" | |
| if [ -n "$cwd" ] && command -v git >/dev/null 2>&1; then | |
| branch=$(git -C "$cwd" --no-optional-locks branch --show-current 2>/dev/null) | |
| fi | |
| # --- PR number and associated issue numbers (via gh CLI, cached) --- | |
| pr_issues_from_pr="" | |
| if [ -n "$branch" ] && [ "$branch" != "main" ] && [ "$branch" != "master" ] && command -v gh >/dev/null 2>&1; then | |
| cache_key=$(echo "$cwd:$branch" | md5 -q 2>/dev/null || echo "$cwd:$branch" | md5sum 2>/dev/null | cut -d' ' -f1) | |
| cache_file="/tmp/claude-statusline-pr-${cache_key}" | |
| if [ ! -f "$cache_file" ] || [ $(( $(date +%s) - $(stat -f%m "$cache_file" 2>/dev/null || stat -c%Y "$cache_file" 2>/dev/null || echo 0) )) -gt 600 ]; then | |
| (cd "$cwd" && gh pr view "$branch" --json number,body 2>/dev/null) > "$cache_file" || echo "" > "$cache_file" | |
| fi | |
| pr_json=$(cat "$cache_file") | |
| if [ -n "$pr_json" ]; then | |
| pr_num=$(echo "$pr_json" | jq -r '.number // empty' 2>/dev/null) | |
| if [ -n "$pr_num" ]; then | |
| pr_body=$(echo "$pr_json" | jq -r '.body // empty' 2>/dev/null) | |
| issue_refs=$(echo "$pr_body" | grep -oiE '(closes|fixes|resolves|references|refs|re)[[:space:]:#]+[0-9]+' | grep -oE '[0-9]+' | sort -un | tr '\n' ',' | sed 's/,$//') | |
| pr_link=$(link "${gh_base}/pull/${pr_num}" "${MAGENTA}PR #${pr_num}${RESET}") | |
| if [ -n "$issue_refs" ]; then | |
| issue_links="" | |
| first=1 | |
| for inum in $(echo "$issue_refs" | tr ',' ' '); do | |
| if [ "$first" -eq 1 ]; then first=0; else issue_links="${issue_links}, "; fi | |
| issue_links="${issue_links}$(link "${gh_base}/issues/${inum}" "#${inum}")" | |
| done | |
| parts+=("${pr_link} ${DIM}(${issue_links})${RESET}") | |
| pr_issues_from_pr="$issue_refs" | |
| else | |
| parts+=("${pr_link}") | |
| fi | |
| fi | |
| fi | |
| fi | |
| # --- GitHub issue number (from branch name) --- | |
| # Convention: <type>/<issue#>-<slug> (e.g., feature/123-user-auth, fix/456-bug) | |
| if [ -n "$branch" ]; then | |
| if echo "$branch" | grep -qE '^(feature|fix|docs|refactor|test|chore|security|hotfix|bug|task|issue|feat)/'; then | |
| issue_raw=$(echo "$branch" | sed -E 's|^[^/]+/||' | grep -oE '^[0-9]+' | head -1) | |
| issue=$(echo "$issue_raw" | sed 's/^0*//') | |
| if [ -n "$issue" ]; then | |
| if [ -z "$pr_issues_from_pr" ] || ! echo ",$pr_issues_from_pr," | grep -q ",$issue,"; then | |
| parts+=("$(link "${gh_base}/issues/${issue}" "${YELLOW}Issue #${issue}${RESET}")") | |
| fi | |
| fi | |
| fi | |
| fi | |
| # --- Context window usage --- | |
| used_pct=$(echo "$input" | jq -r '.context_window.used_percentage // empty') | |
| ctx_size=$(echo "$input" | jq -r '.context_window.context_window_size // empty') | |
| if [ -n "$used_pct" ]; then | |
| used_int=$(printf '%.0f' "$used_pct") | |
| color=$(pct_color "$used_int") | |
| size_str="" | |
| if [ -n "$ctx_size" ]; then | |
| size_k=$(( ctx_size / 1000 )) | |
| size_str=" ${DIM}${size_k}k${RESET}" | |
| fi | |
| parts+=("${DIM}Ctx${RESET} ${color}${used_int}%${RESET}${size_str}") | |
| fi | |
| # --- Session cost --- | |
| cost=$(echo "$input" | jq -r '.cost.total_cost_usd // empty') | |
| if [ -n "$cost" ]; then | |
| cost_fmt=$(printf '$%.2f' "$cost") | |
| parts+=("${DIM}Session${RESET} ${cost_fmt}") | |
| fi | |
| # --- 5-hour rate limit --- | |
| five_pct=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty') | |
| five_reset=$(echo "$input" | jq -r '.rate_limits.five_hour.resets_at // empty') | |
| if [ -n "$five_pct" ]; then | |
| five_int=$(printf '%.0f' "$five_pct") | |
| color=$(pct_color "$five_int") | |
| reset_str="" | |
| if [ -n "$five_reset" ]; then | |
| now=$(date +%s) | |
| diff=$(( five_reset - now )) | |
| if [ "$diff" -le 0 ]; then | |
| reset_str=" ${DIM}now${RESET}" | |
| elif [ "$diff" -lt 3600 ]; then | |
| mins=$(( diff / 60 )) | |
| reset_str=" ${DIM}${mins}m${RESET}" | |
| else | |
| hrs=$(( diff / 3600 )) | |
| mins=$(( (diff % 3600) / 60 )) | |
| if [ "$mins" -gt 0 ]; then | |
| reset_str=" ${DIM}${hrs}h${mins}m${RESET}" | |
| else | |
| reset_str=" ${DIM}${hrs}h${RESET}" | |
| fi | |
| fi | |
| fi | |
| parts+=("${DIM}5h${RESET} ${color}${five_int}%${RESET}${reset_str}") | |
| fi | |
| # --- 7-day rate limit --- | |
| seven_pct=$(echo "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty') | |
| seven_reset=$(echo "$input" | jq -r '.rate_limits.seven_day.resets_at // empty') | |
| if [ -n "$seven_pct" ]; then | |
| seven_int=$(printf '%.0f' "$seven_pct") | |
| color=$(pct_color "$seven_int") | |
| reset_str="" | |
| if [ -n "$seven_reset" ]; then | |
| now=$(date +%s) | |
| diff=$(( seven_reset - now )) | |
| if [ "$diff" -le 0 ]; then | |
| reset_str=" ${DIM}now${RESET}" | |
| elif [ "$diff" -lt 3600 ]; then | |
| mins=$(( diff / 60 )) | |
| reset_str=" ${DIM}${mins}m${RESET}" | |
| elif [ "$diff" -lt 86400 ]; then | |
| hrs=$(( diff / 3600 )) | |
| mins=$(( (diff % 3600) / 60 )) | |
| if [ "$mins" -gt 0 ]; then | |
| reset_str=" ${DIM}${hrs}h${mins}m${RESET}" | |
| else | |
| reset_str=" ${DIM}${hrs}h${RESET}" | |
| fi | |
| else | |
| days=$(( diff / 86400 )) | |
| hrs=$(( (diff % 86400) / 3600 )) | |
| if [ "$hrs" -gt 0 ]; then | |
| reset_str=" ${DIM}${days}d${hrs}h${RESET}" | |
| else | |
| reset_str=" ${DIM}${days}d${RESET}" | |
| fi | |
| fi | |
| fi | |
| parts+=("${DIM}7d${RESET} ${color}${seven_int}%${RESET}${reset_str}") | |
| fi | |
| # --- Org name + subscription (cached 5 min, refreshes after /login) --- | |
| org_cache="/tmp/claude-statusline-org" | |
| if [ ! -f "$org_cache" ] || [ $(( $(date +%s) - $(stat -f%m "$org_cache" 2>/dev/null || stat -c%Y "$org_cache" 2>/dev/null || echo 0) )) -gt 300 ]; then | |
| if command -v claude >/dev/null 2>&1; then | |
| claude auth status 2>/dev/null | jq -r '"\(.orgName // "")\t\(.subscriptionType // "")"' > "$org_cache" 2>/dev/null | |
| fi | |
| fi | |
| org_line=$(cat "$org_cache" 2>/dev/null) | |
| if [ -n "$org_line" ]; then | |
| org_name=$(echo "$org_line" | cut -f1) | |
| sub_type=$(echo "$org_line" | cut -f2) | |
| # Strip trailing "Organization" (with optional leading space/'s) | |
| org_name=$(echo "$org_name" | sed -E "s/['\u2019]?s?[[:space:]]*Organization[[:space:]]*$//") | |
| org_display="${CYAN}${org_name}${RESET}" | |
| if [ -n "$sub_type" ]; then | |
| org_display="${org_display} ${DIM}(${sub_type})${RESET}" | |
| fi | |
| parts+=("${DIM}Org${RESET} ${org_display}") | |
| fi | |
| # --- Output all parts joined by separator --- | |
| if [ "${#parts[@]}" -gt 0 ]; then | |
| output="" | |
| for i in "${!parts[@]}"; do | |
| if [ "$i" -eq 0 ]; then | |
| output="${parts[$i]}" | |
| else | |
| output="${output}${SEP}${parts[$i]}" | |
| fi | |
| done | |
| echo -e "$output" | |
| fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment