Skip to content

Instantly share code, notes, and snippets.

@ewilderj
Last active March 9, 2026 05:11
Show Gist options
  • Select an option

  • Save ewilderj/948f897c411ffb0cc18b381cd35461f8 to your computer and use it in GitHub Desktop.

Select an option

Save ewilderj/948f897c411ffb0cc18b381cd35461f8 to your computer and use it in GitHub Desktop.
Copilot CLI session tracking: git commit hook + resume script

Copilot CLI Session Tracking via Git

Two small tools that link your git commits to Copilot CLI sessions, and let you resume where you left off.

How it works

  1. prepare-commit-msg — a global git hook that appends a Copilot-Session trailer to every commit made during an active Copilot CLI session. The format is <session-id>@<host-hash>, where the host hash (sha256 prefix) prevents leaking hostnames into public repos. Auto-detects running sessions (or prompts if there are multiple).

  2. copilot-git-resume — reads that trailer from the most recent commit and resumes the session, if you're on the same host. Passes through any extra args to copilot.

Setup

# 1. Install the global git hook
mkdir -p ~/bin/git-hooks
cp prepare-commit-msg ~/bin/git-hooks/
chmod +x ~/bin/git-hooks/prepare-commit-msg
git config --global core.hooksPath ~/bin/git-hooks

# 2. Install the resume script
cp copilot-git-resume ~/bin/
chmod +x ~/bin/copilot-git-resume

Usage

# Work normally — commits get tagged automatically:
git commit -m "Fix the thing"
# → Copilot-Session: 2f291d8a-cff7-48ef-a15b-05e98c9bfbc3@f587e31d

# Later, resume from where you left off:
cd my-project
copilot-git-resume

# With extra args:
copilot-git-resume --yolo --model claude-opus-4.6

What the trailers look like

Fix the thing

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 2f291d8a-cff7-48ef-a15b-05e98c9bfbc3@f587e31d

Notes

  • The hook detects active sessions by checking ~/.copilot/session-state/ for recently modified events.jsonl files (within 5 minutes).
  • If COPILOT_SESSION_ID is set in the environment, it uses that directly.
  • The host hash is the first 8 chars of sha256(hostname -s) — enough for matching without revealing the machine name.
  • copilot-git-resume also handles legacy commits with bare session IDs or the old two-trailer format (Copilot-Session + Copilot-Host).
  • The hook chains to per-repo hooks if they exist at .git/hooks/prepare-commit-msg.
#!/bin/bash
# copilot-git-resume — Resume a Copilot session from the last git commit.
#
# Reads the Copilot-Session trailer (<session-id>@<host-hash>) from
# the most recent commit. If the host hash matches this machine, runs
# `copilot --resume <SESSION>` with any extra arguments passed through.
#
# Usage:
# copilot-git-resume [extra copilot args...]
#
# Examples:
# copilot-git-resume
# copilot-git-resume --model claude-opus-4.6
# copilot-git-resume --yolo
set -euo pipefail
# Extract trailer from the most recent commit
TRAILER=$(git --no-pager log -1 --format='%(trailers:key=Copilot-Session,valueonly)' 2>/dev/null | head -1)
if [[ -z "$TRAILER" ]]; then
echo "error: no Copilot-Session trailer found in the last commit" >&2
exit 1
fi
# Parse <session-id>@<host-hash>
if [[ "$TRAILER" == *@* ]]; then
SESSION="${TRAILER%@*}"
HOST_HASH="${TRAILER##*@}"
else
# Legacy format: bare session ID (no host check possible)
SESSION="$TRAILER"
HOST_HASH=""
fi
if [[ -z "$SESSION" ]]; then
echo "error: could not parse session ID from trailer: $TRAILER" >&2
exit 1
fi
# Verify host if hash is present
if [[ -n "$HOST_HASH" ]]; then
LOCAL_HASH=$(hostname -s | shasum -a 256 | cut -c1-8)
if [[ "$HOST_HASH" != "$LOCAL_HASH" ]]; then
echo "error: session is from a different host (hash $HOST_HASH, local $LOCAL_HASH)" >&2
exit 1
fi
fi
# Verify the session state directory exists
SESSION_STATE="$HOME/.copilot/session-state/$SESSION"
if [[ ! -d "$SESSION_STATE" ]]; then
echo "error: session state directory not found: $SESSION_STATE" >&2
exit 1
fi
exec copilot --resume "$SESSION" "$@"
#!/bin/bash
# Append Copilot session trailers to commit messages.
#
# Session detection priority:
# 1. COPILOT_SESSION_ID env var (set by agent or IDE)
# 2. Single recently-active session (auto-detected)
# 3. Multiple active sessions → interactive chooser via /dev/tty
#
# Also chains to per-repo hooks if they exist.
# Install: git config --global core.hooksPath ~/bin/git-hooks
COMMIT_MSG_FILE="$1"
COMMIT_SOURCE="$2" # message, template, merge, squash, commit (amend)
SHA1="$3"
# --- Chain to per-repo hook if it exists ---
chain_to_repo_hook() {
local repo_hook
repo_hook="$(git rev-parse --git-dir 2>/dev/null)/hooks/prepare-commit-msg"
if [[ -x "$repo_hook" ]]; then
exec "$repo_hook" "$@"
fi
exit 0
}
# Skip merges — don't overwrite existing trailers
[[ "$COMMIT_SOURCE" == "merge" ]] && chain_to_repo_hook "$@"
# Don't duplicate if trailers already present (e.g., amend)
if grep -q "Copilot-Session:" "$COMMIT_MSG_FILE" 2>/dev/null; then
chain_to_repo_hook "$@"
fi
SESSION_DIR="$HOME/.copilot/session-state"
SESSION_ID=""
# --- Priority 1: Env var (agent or IDE sets this) ---
if [[ -n "$COPILOT_SESSION_ID" ]]; then
SESSION_ID="$COPILOT_SESSION_ID"
# --- Priority 2/3: Auto-detect from session state ---
elif [[ -d "$SESSION_DIR" ]]; then
# Use find -mmin to let the filesystem filter by recency — avoids
# statting thousands of old session dirs. Extract UUIDs from paths.
ACTIVE_SESSIONS=()
while IFS= read -r file; do
SID=$(echo "$file" | grep -oE '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')
[[ -n "$SID" ]] && ACTIVE_SESSIONS+=("$SID")
done < <(find "$SESSION_DIR" -name "events.jsonl" -maxdepth 2 -mmin -5 2>/dev/null)
case ${#ACTIVE_SESSIONS[@]} in
0)
# No active sessions — skip trailers
;;
*)
# One or more active sessions — ask the user to confirm or skip
if [[ -t 0 ]] || [[ -e /dev/tty ]]; then
echo "" >&2
echo "Active Copilot session(s) detected:" >&2
for i in "${!ACTIVE_SESSIONS[@]}"; do
SID="${ACTIVE_SESSIONS[$i]}"
# Show short ID + checkpoint title if available
LABEL="$SID"
CHECKPOINT_IDX="$SESSION_DIR/$SID/checkpoints/index.md"
if [[ -f "$CHECKPOINT_IDX" ]]; then
TITLE=$(tail -1 "$CHECKPOINT_IDX" | sed 's/.*- //')
[[ -n "$TITLE" ]] && LABEL="$SID ($TITLE)"
fi
echo " $((i+1)). $LABEL" >&2
done
echo " s. Skip (no session trailer)" >&2
printf "Which session? [1-%d/s]: " "${#ACTIVE_SESSIONS[@]}" >&2
read -r choice < /dev/tty
if [[ "$choice" == "s" || "$choice" == "S" ]]; then
SESSION_ID=""
elif [[ "$choice" -ge 1 && "$choice" -le ${#ACTIVE_SESSIONS[@]} ]] 2>/dev/null; then
SESSION_ID="${ACTIVE_SESSIONS[$((choice-1))]}"
else
# Default to most recent
SESSION_ID="${ACTIVE_SESSIONS[0]}"
fi
else
# Non-interactive — use most recent
SESSION_ID="${ACTIVE_SESSIONS[0]}"
fi
;;
esac
fi
# --- Append trailer ---
# Format: <session-id>@<host-hash> — the hash avoids leaking hostnames
# into public repos while still allowing host matching for resume.
if [[ -n "$SESSION_ID" ]]; then
HOST_HASH=$(hostname -s | shasum -a 256 | cut -c1-8)
printf '\nCopilot-Session: %s@%s\n' "$SESSION_ID" "$HOST_HASH" >> "$COMMIT_MSG_FILE"
fi
chain_to_repo_hook "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment