
Coaching Session Summarizer
- 56 installs
- 339 repo stars
- Updated August 4, 2026
- glebis/claude-skills
Summarize a coaching or therapy session transcript in-session and append structured insights, decisions, action items, and cross-session patterns to the note.
About
Reads a synced Fathom/Granola transcript, analyzes it directly on the subscription with no API call, and appends key insights, decisions, action items, and optional deep cross-session analysis. A developer uses it after a session syncs to their Obsidian vault to get a structured summary linked to trails.
- Agent-driven analysis, no Anthropic API key or billing
- Quick vs deep mode with cross-session pattern detection
Coaching Session Summarizer by the numbers
- 56 all-time installs (skills.sh)
- Ranked #1,583 of 3,282 Productivity & Planning skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/glebis/claude-skills --skill coaching-session-summarizerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 56 |
|---|---|
| repo stars | ★ 339 |
| Last updated | August 4, 2026 |
| Repository | glebis/claude-skills ↗ |
What it does
Summarize a coaching or therapy session transcript in-session and append structured insights, decisions, action items, and cross-session patterns to the note.
Files
Coaching Session Summarizer
Overview
Analyzes a coaching/therapy session transcript and appends a structured summary (key insights, decisions, action items, deep analysis, connected trails) to the note.
The agent (Claude Code) performs the analysis directly — reading the transcript and writing the summary in this session. There is no Anthropic API call and no billing; it runs entirely on the active subscription. A legacy API-based script is kept only as a headless fallback (see bottom).
When to Use This Skill
- A new Fathom/Granola transcript was synced to the vault (coaching or therapy)
- User asks to summarize/analyze a session (
/summarize-session [file]or similar) - After
calendar-syncor a Granola export, when a new*-coaching.md,
*-therapy.md, or *-session.md file appears — offer to summarize it
Workflow (agent-driven — default)
Do this in-session with native tools. No API key required.
Step 1 — Gather context
Run the deterministic helper to get the transcript text, previous sessions, and the trail list in one shot:
python3 ~/.claude/skills/coaching-session-summarizer/scripts/gather_context.py \
<transcript-file> --vault ~/Brains/brainIt prints:
- Previous sessions with the same participant (paths) —
Readthese only in
deep mode, for cross-session pattern detection
- Available trails — pick 2–4 most relevant to link
- Session content — the summary + transcript to analyze (any prior
AI-Generated Summary is stripped so re-runs stay clean)
Pass --participant <name-slug> if the filename doesn't encode the person (e.g. Granola exports titled by topic): --participant gleb-kalinin.
Step 2 — Analyze
Read the session content and extract, in the analytical voice of a session analyst (objective, using the speaker's authentic language where it matters):
- Key Insights — 3–5 main realizations / breakthroughs / observations
- Decisions Made — concrete choices or commitments
- Action Items — specific next steps; prefix time-sensitive ones with
[URGENT] and scheduling items with [SCHEDULING]
- Session Themes — 2–3 recurring topics or patterns
Deep mode (default for therapy and milestone sessions) — also Read the previous sessions and add:
- Pattern Detection — themes recurring across sessions
- Progress Assessment — movement on earlier commitments
- Energy/Motivation Markers — shifts in energy, resistance, affect
- Potential Obstacles — what might block progress
Step 3 — Append with Edit
Append the summary to the end of the transcript file using Edit (never overwrite existing content). Match this exact structure:
## AI-Generated Summary
*Generated: YYYY-MM-DD*
### Key Insights
- ...
### Decisions Made
- ...
### Action Items
- [URGENT] ...
- ...
### Session Themes
- ...
## Deep Analysis
- **Pattern Detection**: ...
- **Progress Assessment**: ...
- **Energy/Motivation Markers**: ...
- **Potential Obstacles**: ...
## Connected Trails
- [[Trails/Trail - <Name>|<Name>]]
- [[Trails/Trail - <Name>|<Name>]]Use the current date (date +%Y-%m-%d) in the Generated line. Omit the Deep Analysis section in quick mode. Verify trail link names against the printed trail list — case and exact wording matter for Obsidian links.
Modes
- quick — Key Insights, Decisions, Action Items, Themes. Skip Deep Analysis
and previous-session reads.
- deep (recommended for therapy / milestones) — everything, including
reading previous sessions for pattern detection.
Integration with Sync
After calendar-sync or a Granola/Fathom export, check for new session files (*-coaching.md, *-therapy.md, *-session.md). If one appears, offer: "New session detected — summarize now?" Default to deep mode for therapy.
Notes
- Preserves the original transcript intact; the summary is always appended.
- Trail linking requires the
Trails/directory in the vault root. - Cross-session comparison works best with consistent naming:
YYYYMMDD-name-coaching.md / YYYYMMDD-name-therapy.md.
- Re-running is safe:
gather_context.pystrips any prior AI-Generated Summary
before printing, so the agent analyzes only the raw session. (Delete the old ## AI-Generated Summary block from the file before re-appending if you want to replace rather than stack summaries.)
Resources
scripts/
- gather_context.py — (default path) deterministic context gatherer, no
API. Prints transcript text + previous sessions + trail list for the agent to analyze in-session.
- summarize_session.py — legacy / headless fallback. Calls the Anthropic
API directly (model via SUMMARIZER_MODEL, default claude-sonnet-4-6) and bills a funded ANTHROPIC_API_KEY. Use only when no interactive agent is available (e.g. cron). Exits with a clear message if the key has no credit.
#!/usr/bin/env python3
"""
Gather context for agent-driven session analysis (no API calls).
Prints everything the agent needs to analyze a coaching/therapy session:
- the transcript text (summary + transcript, summaries excluded)
- up to 3 previous sessions with the same participant (paths)
- the list of available trails (for linking)
The agent (Claude Code) reads this output, performs the analysis itself, and
appends the result to the transcript file with Edit. This keeps the whole
workflow on the subscription — no Anthropic API key, no billing.
"""
import argparse
import os
import re
import sys
from pathlib import Path
def load_transcript(file_path):
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
parts = content.split("---", 2)
if len(parts) >= 3:
return parts[1], parts[2]
return "", content
def extract_transcript_only(body):
"""Transcript + Summary sections, but drop any prior AI-Generated Summary."""
body = re.split(r"\n## AI-Generated Summary", body, maxsplit=1)[0]
return body.strip()
def get_previous_sessions(vault_path, current_session, participant_name):
vault = Path(vault_path)
pattern = f"*{participant_name.lower().replace(' ', '-')}*.md"
sessions = []
for f in vault.glob(pattern):
stem = f.stem
if f.name != current_session and (
"coaching" in stem or "therapy" in stem or "session" in stem
):
sessions.append(f)
sessions.sort(key=lambda x: x.stem[:8] if x.stem[:8].isdigit() else "0")
return sessions[-3:]
def find_trails(vault_path):
trails_dir = Path(vault_path) / "Trails"
if not trails_dir.exists():
return []
return sorted(
t.stem.replace("Trail - ", "") for t in trails_dir.glob("Trail*.md")
)
def main():
ap = argparse.ArgumentParser(description="Gather context for agent-driven analysis")
ap.add_argument("transcript_file")
ap.add_argument("--vault", default=os.path.expanduser("~/Brains/brain"))
ap.add_argument(
"--participant",
default="",
help="Participant name for previous-session lookup (e.g. 'gleb-kalinin'). "
"Falls back to the filename's name segment.",
)
args = ap.parse_args()
path = Path(args.transcript_file)
if not path.exists():
print(f"Error: file not found: {path}", file=sys.stderr)
sys.exit(1)
frontmatter, body = load_transcript(path)
transcript = extract_transcript_only(body)
participant = args.participant
if not participant:
# Heuristic: strip leading YYYYMMDD- and trailing -<sessiontype> from stem.
stem = re.sub(r"^\d{8}-", "", path.stem)
stem = re.sub(r"-(coaching|therapy|session|call|meeting|workshop)$", "", stem)
participant = stem
prev = get_previous_sessions(args.vault, path.name, participant)
trails = find_trails(args.vault)
print("=" * 70)
print("PREVIOUS SESSIONS (read these for cross-session pattern detection):")
if prev:
for p in prev:
print(f" - {p}")
else:
print(" (none found)")
print()
print("AVAILABLE TRAILS (link 2-4 most relevant):")
print(" " + ", ".join(trails) if trails else " (none found)")
print("=" * 70)
print()
print("SESSION CONTENT TO ANALYZE:")
print()
print(transcript)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Coaching Session Summarizer
Extracts key insights, decisions, action items, and trail connections from
coaching session transcripts using LLM analysis.
"""
import anthropic
import argparse
import os
import sys
from pathlib import Path
import re
from datetime import datetime
# Centralized model — bump here when migrating. As of 2026-05 the previous
# pin (claude-sonnet-4-20250514) was deprecated (EOL 2026-06-15).
MODEL = os.environ.get("SUMMARIZER_MODEL", "claude-sonnet-4-6")
def load_transcript(file_path):
"""Load transcript content from markdown file."""
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Extract frontmatter and transcript
parts = content.split('---', 2)
if len(parts) >= 3:
frontmatter = parts[1]
body = parts[2]
else:
frontmatter = ""
body = content
return frontmatter, body
def extract_transcript_only(body):
"""Extract just the transcript section, excluding summaries."""
# Find transcript section
transcript_match = re.search(r'## Transcript\s*\n(.*?)(?:\n## |$)', body, re.DOTALL)
if transcript_match:
return transcript_match.group(1).strip()
return body.strip()
def get_previous_sessions(vault_path, current_session, participant_name):
"""Find previous coaching sessions with same participant."""
vault = Path(vault_path)
pattern = f"*{participant_name.lower().replace(' ', '-')}*.md"
sessions = []
for session_file in vault.glob(pattern):
if 'coaching' in session_file.stem and session_file.name != current_session:
sessions.append(session_file)
# Sort by date (YYYYMMDD prefix)
sessions.sort(key=lambda x: x.stem[:8] if x.stem[:8].isdigit() else '0')
return sessions[-3:] if len(sessions) > 3 else sessions # Last 3 sessions
def find_trails(vault_path):
"""Load all trail files for semantic matching."""
trails_dir = Path(vault_path) / "Trails"
if not trails_dir.exists():
return []
trails = []
for trail_file in trails_dir.glob("Trail*.md"):
with open(trail_file, 'r', encoding='utf-8') as f:
content = f.read()
# Extract title from filename
title = trail_file.stem.replace('Trail - ', '')
trails.append({
'name': title,
'file': trail_file.name,
'preview': content[:500] # First 500 chars for context
})
return trails
def quick_extract(client, transcript, mode="quick"):
"""Perform quick extraction of key elements."""
system_prompt = """You are a coaching session analyst. Extract key information concisely and objectively.
Focus on:
1. Key Insights: Main realizations, breakthroughs, or important observations (3-5 items)
2. Decisions Made: Concrete choices or commitments the coachee made
3. Action Items: Specific next steps with implied urgency/timeline
4. Themes: Recurring topics or patterns
Be concise and use the coachee's authentic language where meaningful.
Output in markdown format with clear sections."""
prompt = f"""Analyze this coaching session transcript and extract:
## Key Insights
- List 3-5 main realizations or important observations
## Decisions Made
- List concrete decisions or commitments
## Action Items
- List specific next steps (mark urgent items with [URGENT] prefix)
## Session Themes
- Note 2-3 recurring topics or patterns
Transcript:
{transcript}"""
message = client.messages.create(
model=MODEL,
max_tokens=2000,
system=system_prompt,
messages=[{"role": "user", "content": prompt}]
)
return message.content[0].text
def deep_analysis(client, transcript, previous_sessions_summary, trails):
"""Perform deeper analysis with pattern detection and trail matching."""
trails_context = "\n".join([f"- {t['name']}: {t['preview'][:200]}..." for t in trails])
system_prompt = """You are a coaching session analyst performing deep analysis.
Focus on:
1. Patterns across sessions
2. Progress on previous commitments
3. Emotional/energy shifts
4. Connections to existing trails/projects
5. Potential obstacles or blockers
Be insightful while remaining objective."""
prompt = f"""Perform deep analysis of this coaching session:
## Context
Previous sessions summary:
{previous_sessions_summary if previous_sessions_summary else "No previous sessions available"}
Available Trails (projects/areas of focus):
{trails_context if trails_context else "No trails found"}
## Analysis Required
1. **Pattern Detection**: What themes recur across sessions?
2. **Progress Assessment**: How has the coachee progressed on previous commitments?
3. **Trail Connections**: Which trails/projects are relevant to this session? (Use exact trail names)
4. **Energy/Motivation Markers**: Note shifts in energy, enthusiasm, or resistance
5. **Potential Obstacles**: What might block progress?
Transcript:
{transcript}
Provide analysis in markdown with clear sections."""
message = client.messages.create(
model=MODEL,
max_tokens=3000,
system=system_prompt,
messages=[{"role": "user", "content": prompt}]
)
return message.content[0].text
def match_trails(client, summary, trails):
"""Match session content to relevant trails."""
if not trails:
return []
trails_list = "\n".join([f"- {t['name']}" for t in trails])
prompt = f"""Based on this session summary, identify the 2-4 most relevant trails (projects/focus areas):
Available Trails:
{trails_list}
Session Summary:
{summary}
Output ONLY the trail names, one per line, no explanations."""
message = client.messages.create(
model=MODEL,
max_tokens=500,
messages=[{"role": "user", "content": prompt}]
)
matched = [line.strip('- ').strip() for line in message.content[0].text.split('\n') if line.strip()]
return matched
def format_output(frontmatter, quick_summary, deep_summary=None, matched_trails=None):
"""Format the final output with enhanced frontmatter and sections."""
# Parse existing frontmatter
fm_dict = {}
for line in frontmatter.split('\n'):
if ':' in line:
key, value = line.split(':', 1)
fm_dict[key.strip()] = value.strip()
# Add trails if matched
if matched_trails:
trails_links = '\n - '.join([f'"[[Trails/Trail - {t}|{t}]]"' for t in matched_trails])
fm_dict['trails'] = f'\n - {trails_links}'
# Reconstruct frontmatter
new_fm = "---\n"
for key, value in fm_dict.items():
new_fm += f"{key}: {value}\n"
new_fm += "---\n\n"
# Build output
output = new_fm
output += "## AI-Generated Summary\n\n"
output += f"*Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}*\n\n"
output += quick_summary
if deep_summary:
output += "\n\n## Deep Analysis\n\n"
output += deep_summary
if matched_trails:
output += "\n\n## Connected Trails\n\n"
for trail in matched_trails:
output += f"- [[Trails/Trail - {trail}|{trail}]]\n"
return output
def main():
parser = argparse.ArgumentParser(description='Summarize coaching session transcript')
parser.add_argument('transcript_file', help='Path to transcript markdown file')
parser.add_argument('--vault', default=os.path.expanduser('~/Brains/brain'),
help='Path to Obsidian vault')
parser.add_argument('--mode', choices=['quick', 'deep', 'hybrid'], default='hybrid',
help='Analysis mode')
parser.add_argument('--output', help='Output file (default: append to input file)')
args = parser.parse_args()
# Get API key
api_key = os.environ.get('ANTHROPIC_API_KEY')
if not api_key:
print("Error: ANTHROPIC_API_KEY not set", file=sys.stderr)
sys.exit(1)
client = anthropic.Anthropic(api_key=api_key)
# Load transcript
print(f"Loading transcript: {args.transcript_file}")
frontmatter, body = load_transcript(args.transcript_file)
transcript = extract_transcript_only(body)
# Extract participant name from frontmatter
participant_match = re.search(r'coach:\s*"?\[\[@?([^\]|]+)', frontmatter)
participant_name = participant_match.group(1) if participant_match else "Unknown"
try:
# Quick extract
print(f"Performing quick extraction (model: {MODEL})...")
quick_summary = quick_extract(client, transcript, mode=args.mode)
deep_summary = None
matched_trails = None
if args.mode in ['deep', 'hybrid']:
# Find previous sessions and trails
print("Loading context for deep analysis...")
previous_sessions = get_previous_sessions(args.vault,
Path(args.transcript_file).name,
participant_name)
trails = find_trails(args.vault)
prev_summary = ""
if previous_sessions:
prev_summary = f"Previous {len(previous_sessions)} sessions found"
# Deep analysis
print("Performing deep analysis...")
deep_summary = deep_analysis(client, transcript, prev_summary, trails)
# Match trails
print("Matching relevant trails...")
matched_trails = match_trails(client, quick_summary, trails)
except anthropic.BadRequestError as e:
if "credit balance is too low" in str(e):
print(
"\nError: the ANTHROPIC_API_KEY in this environment has no credit "
"balance.\nThis script bills the Anthropic API directly.\n"
"Options:\n"
" 1. Add credits / use a funded key (export ANTHROPIC_API_KEY=...).\n"
" 2. Ask Claude Code to run the analysis in-session (no API billing).",
file=sys.stderr,
)
sys.exit(2)
raise
except anthropic.AuthenticationError:
print("\nError: ANTHROPIC_API_KEY is invalid or expired.", file=sys.stderr)
sys.exit(2)
# Format output
output = format_output(frontmatter, quick_summary, deep_summary, matched_trails)
# Write output
output_file = args.output or args.transcript_file
if output_file == args.transcript_file:
# Append to existing file
with open(output_file, 'a', encoding='utf-8') as f:
f.write("\n\n" + output)
print(f"\n✓ Summary appended to {output_file}")
else:
# Write new file
with open(output_file, 'w', encoding='utf-8') as f:
f.write(output)
print(f"\n✓ Summary written to {output_file}")
print("\nSummary complete!")
if matched_trails:
print(f"Connected trails: {', '.join(matched_trails)}")
if __name__ == '__main__':
main()