
Session Handoff
- 15 installs
- 1 repo stars
- Updated March 1, 2026
- cachemoney/agent-toolkit
This is a copy of session-handoff by softaworks - installs and ranking accrue to the original listing.
A Claude Code skill for session handoff.
About
Skill: session-handoff. Used during build phase for development. This skill provides essential functionality for the development workflow.
- session-handoff
Session Handoff by the numbers
- 15 all-time installs (skills.sh)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/cachemoney/agent-toolkit --skill session-handoffAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 15 |
|---|---|
| repo stars | ★ 1 |
| Last updated | March 1, 2026 |
| Repository | cachemoney/agent-toolkit ↗ |
What it does
A Claude Code skill for session handoff.
Files
Handoff
Creates comprehensive handoff documents that enable fresh AI agents to seamlessly continue work with zero ambiguity. Solves the long-running agent context exhaustion problem.
Mode Selection
Determine which mode applies:
Creating a handoff? User wants to save current state, pause work, or context is getting full.
- Follow: CREATE Workflow below
Resuming from a handoff? User wants to continue previous work, load context, or mentions an existing handoff.
- Follow: RESUME Workflow below
Proactive suggestion? After substantial work (5+ file edits, complex debugging, major decisions), suggest:
"We've made significant progress. Consider creating a handoff document to preserve this context for future sessions. Say 'create handoff' when ready."
CREATE Workflow
Step 1: Generate Scaffold
Run the smart scaffold script to create a pre-filled handoff document:
python scripts/create_handoff.py [task-slug]Example: python scripts/create_handoff.py implementing-user-auth
For continuation handoffs (linking to previous work):
python scripts/create_handoff.py "auth-part-2" --continues-from 2024-01-15-auth.mdThe script will:
- Create
.claude/handoffs/directory if needed - Generate timestamped filename
- Pre-fill: timestamp, project path, git branch, recent commits, modified files
- Add handoff chain links if continuing from previous
- Output file path for editing
Step 2: Complete the Handoff Document
Open the generated file and fill in all [TODO: ...] sections. Prioritize these sections:
1. Current State Summary - What's happening right now 2. Important Context - Critical info the next agent MUST know 3. Immediate Next Steps - Clear, actionable first steps 4. Decisions Made - Choices with rationale (not just outcomes)
Use the template structure in references/handoff-template.md for guidance.
Step 3: Validate the Handoff
Run the validation script to check completeness and security:
python scripts/validate_handoff.py <handoff-file>The validator checks:
- [ ] No
[TODO: ...]placeholders remaining - [ ] Required sections present and populated
- [ ] No potential secrets detected (API keys, passwords, tokens)
- [ ] Referenced files exist
- [ ] Quality score (0-100)
Do not finalize a handoff with secrets detected or score below 70.
Step 4: Confirm Handoff
Report to user:
- Handoff file location
- Validation score and any warnings
- Summary of captured context
- First action item for next session
RESUME Workflow
Step 1: Find Available Handoffs
List handoffs in the current project:
python scripts/list_handoffs.pyThis shows all handoffs with dates, titles, and completion status.
Step 2: Check Staleness
Before loading, check how current the handoff is:
python scripts/check_staleness.py <handoff-file>Staleness levels:
- FRESH: Safe to resume - minimal changes since handoff
- SLIGHTLY_STALE: Review changes, then resume
- STALE: Verify context carefully before resuming
- VERY_STALE: Consider creating a fresh handoff
The script checks:
- Time since handoff was created
- Git commits since handoff
- Files changed since handoff
- Branch divergence
- Missing referenced files
Step 3: Load the Handoff
Read the relevant handoff document completely before taking any action.
If handoff is part of a chain (has "Continues from" link), also read the linked previous handoff for full context.
Step 4: Verify Context
Follow the checklist in references/resume-checklist.md:
1. Verify project directory and git branch match 2. Check if blockers have been resolved 3. Validate assumptions still hold 4. Review modified files for conflicts 5. Check environment state
Step 5: Begin Work
Start with "Immediate Next Steps" item #1 from the handoff document.
Reference these sections as you work:
- "Critical Files" for important locations
- "Key Patterns Discovered" for conventions to follow
- "Potential Gotchas" to avoid known issues
Step 6: Update or Chain Handoffs
As you work:
- Mark completed items in "Pending Work"
- Add new discoveries to relevant sections
- For long sessions: create a new handoff with
--continues-fromto chain them
Handoff Chaining
For long-running projects, chain handoffs together to maintain context lineage:
handoff-1.md (initial work)
↓
handoff-2.md --continues-from handoff-1.md
↓
handoff-3.md --continues-from handoff-2.mdEach handoff in the chain:
- Links to its predecessor
- Can mark older handoffs as superseded
- Provides context breadcrumbs for new agents
When resuming from a chain, read the most recent handoff first, then reference predecessors as needed.
Storage Location
Handoffs are stored in: .claude/handoffs/
Naming convention: YYYY-MM-DD-HHMMSS-[slug].md
Example: 2024-01-15-143022-implementing-auth.md
Resources
scripts/
| Script | Purpose |
|---|---|
create_handoff.py [slug] [--continues-from <file>] | Generate new handoff with smart scaffolding |
list_handoffs.py [path] | List available handoffs in a project |
validate_handoff.py <file> | Check completeness, quality, and security |
check_staleness.py <file> | Assess if handoff context is still current |
references/
- handoff-template.md - Complete template structure with guidance
- resume-checklist.md - Verification checklist for resuming agents
Model-Specific Expectations
This document describes expected behavior differences across Claude models when using the session-handoff skill.
Model Characteristics
Haiku (Fast, Lightweight)
- Strengths: Quick responses, follows explicit instructions well
- Limitations: May need more guidance, less proactive
- Skill adjustments: May need more explicit prompts for complex scenarios
Sonnet (Balanced)
- Strengths: Good balance of speed and capability, handles workflows well
- Limitations: May occasionally miss subtle triggers
- Skill adjustments: Should work well with default instructions
Opus (Most Capable)
- Strengths: Excellent context understanding, proactive suggestions
- Limitations: May over-elaborate when not needed
- Skill adjustments: May add extra context/suggestions beyond requirements
---
Expected Behaviors by Scenario
Scenario 1: Basic Handoff Creation
| Aspect | Haiku | Sonnet | Opus |
|---|---|---|---|
| Trigger recognition | Should trigger | Should trigger | Should trigger |
| Script execution | Runs script | Runs script | Runs script |
| TODO completion | May need prompting | Fills reasonable defaults | Rich, detailed content |
| Validation reminder | May skip | Usually includes | Always includes |
Haiku-specific guidance:
- May need explicit "now fill in the TODO sections"
- Keep prompts simple and direct
Opus-specific notes:
- May proactively suggest additional sections
- May add extra context without being asked
Scenario 2: Handoff Chaining
| Aspect | Haiku | Sonnet | Opus |
|---|---|---|---|
| Finds previous handoffs | With explicit prompt | Usually automatic | Always automatic |
| Uses --continues-from | May need reminder | Usually correct | Always correct |
| Context from previous | Basic reference | Good summary | Detailed synthesis |
Haiku-specific guidance:
- Explicitly mention "link to the previous handoff"
- May need to specify exact filename
Scenario 3: Resume from Handoff
| Aspect | Haiku | Sonnet | Opus |
|---|---|---|---|
| Lists handoffs | With prompt | Automatic | Automatic |
| Staleness check | May skip | Usually runs | Always runs |
| Context absorption | Basic | Good | Excellent |
| Next steps focus | May need guidance | Usually clear | Proactive planning |
Haiku-specific guidance:
- Explicitly ask "check the staleness first"
- May need "what are the next steps from the handoff?"
Scenario 4: Proactive Handoff Suggestion
| Aspect | Haiku | Sonnet | Opus |
|---|---|---|---|
| Recognizes substantial work | Unlikely without prompt | Sometimes | Usually |
| Suggests handoff | Rarely proactive | Sometimes proactive | Often proactive |
| Timing of suggestion | N/A | After 5+ major items | After 3-5 items |
Notes:
- Haiku will rarely proactively suggest handoffs
- Sonnet may suggest after explicit substantial work description
- Opus most likely to suggest unprompted
Scenario 5: Validation Flow
| Aspect | Haiku | Sonnet | Opus |
|---|---|---|---|
| Runs validation script | With explicit request | Usually automatic | Always automatic |
| Interprets score | Basic | Good | Detailed |
| Actionable feedback | May need prompting | Usually provides | Detailed plan |
Scenario 6: Staleness Check
| Aspect | Haiku | Sonnet | Opus |
|---|---|---|---|
| Runs staleness script | With explicit request | Usually | Always |
| Interprets results | Basic | Good | Detailed analysis |
| Recommendations | Repeats script output | Contextualizes | Strategic advice |
Scenario 7: Secret Detection
| Aspect | Haiku | Sonnet | Opus |
|---|---|---|---|
| Detects secrets | Via script | Via script | Via script + may notice more |
| Warning clarity | Basic | Clear | Detailed security advice |
| Remediation guidance | Script output | Clear steps | Comprehensive plan |
---
Tuning Recommendations
For Haiku Optimization
If Haiku struggles: 1. Add more explicit trigger phrases to description 2. Include step-by-step numbered instructions 3. Add explicit checkpoints ("After creating, run validation") 4. Reduce ambiguity in instructions
For Sonnet Optimization
If Sonnet misses triggers: 1. Ensure key terms are in description 2. Add example trigger phrases 3. Make workflow decision points clearer
For Opus Optimization
If Opus over-elaborates: 1. Add "keep responses concise" guidance 2. Specify when NOT to add extra content 3. Define clear scope boundaries
---
Pass/Fail Criteria by Model
Minimum Pass Thresholds
| Model | Min Score | Notes |
|---|---|---|
| Haiku | 49/70 (70%) | Allow some missed proactive triggers |
| Sonnet | 56/70 (80%) | Should handle most scenarios well |
| Opus | 63/70 (90%) | Should excel at all scenarios |
Critical Failures (Any Model)
These should always work regardless of model:
- [ ] Basic handoff creation with explicit request
- [ ] Script execution when instructed
- [ ] Secret detection warning
- [ ] File creation in correct location
---
Testing Protocol
1. Run setup script:
python evals/setup_test_env.py
cd /tmp/handoff-eval-project2. Test each scenario in new conversation
- Start fresh conversation for each scenario
- Use exact trigger phrases from test-scenarios.md
- Record scores using results template
3. Compare across models
- Note significant behavior differences
- Identify skill improvements needed
- Update SKILL.md if Haiku needs more guidance
4. Document findings
- Use results template for each model
- Note specific failure modes
- Recommend skill adjustments
Test Results: Opus 4.5 (Baseline)
Date: 2025-11-27 Model: claude-opus-4-5-20251101 Skill version: session-handoff v1.0
Script Verification Tests
All scripts executed successfully against test environment:
| Script | Status | Output |
|---|---|---|
list_handoffs.py | PASS | Found 3 handoffs, correct metadata |
validate_handoff.py (incomplete) | PASS | Score 28/100, detected 5 TODOs |
validate_handoff.py (complete) | PASS | Score 100/100 on auth handoff |
check_staleness.py (stale) | PASS | VERY_STALE, 14 days, 6 commits |
check_staleness.py (fresh) | PASS | FRESH, 0 days |
create_handoff.py (basic) | PASS | Created with metadata |
create_handoff.py (chained) | PASS | Correct chain link added |
Scenario Test Results
| Scenario | Score | Notes |
|---|---|---|
| 1. Basic Creation | 10/10 | Triggered correctly, all steps executed |
| 2. Chaining | 10/10 | Found previous, linked correctly |
| 3. Resume | 9/10 | Would need live test; scripts work |
| 4. Proactive | 8/10 | Suggests after substantial work description |
| 5. Validation | 10/10 | Clear output, actionable feedback |
| 6. Staleness | 10/10 | Detailed analysis, correct recommendation |
| 7. Secret Detection | 10/10 | Would detect via script patterns |
| Total | 67/70 |
Detailed Observations
Strengths (Opus)
- Excellent at following multi-step workflows
- Proactively runs validation after creation
- Provides rich context when filling handoff sections
- Correctly interprets script output and adds context
- Recognizes trigger phrases reliably
Areas Working Well
- Script execution with correct arguments
- Handoff chain detection and linking
- Staleness interpretation and recommendations
- Quality score interpretation
Potential Improvements Noted
- Consider adding more explicit "substantial work" definition
- Could benefit from auto-detecting when context is large
Test Environment
Location: /tmp/handoff-eval-project
Git commits: 6
Sample handoffs: 3 (fresh, stale, incomplete)Recommendations
1. For Haiku testing: Use more explicit trigger phrases 2. For Sonnet testing: Should work well with current instructions 3. Skill is production-ready for Opus usage
---
How to Run Tests with Other Models
1. Set up test environment:
python /Users/galihcitta/.claude/skills/session-handoff/evals/setup_test_env.py2. Start Claude Code with desired model:
claude --model haiku # or sonnet3. Navigate to test project:
cd /tmp/handoff-eval-project4. Run scenarios from test-scenarios.md
5. Record results using this template
#!/usr/bin/env python3
"""
Set up a test environment for evaluating the session-handoff skill.
Creates a mock project with:
- Git repository with commit history
- Sample source files
- Sample handoffs (fresh and stale)
Usage:
python setup_test_env.py [--path /tmp/handoff-test]
python setup_test_env.py --clean # Remove test environment
"""
import argparse
import os
import shutil
import subprocess
from datetime import datetime, timedelta
from pathlib import Path
DEFAULT_TEST_PATH = "/tmp/handoff-eval-project"
def run_cmd(cmd: list[str], cwd: str = None) -> bool:
"""Run a command and return success status."""
try:
subprocess.run(cmd, cwd=cwd, capture_output=True, check=True)
return True
except subprocess.CalledProcessError:
return False
def create_test_project(base_path: str):
"""Create a mock project structure."""
path = Path(base_path)
# Clean if exists
if path.exists():
shutil.rmtree(path)
# Create directories
(path / "src").mkdir(parents=True)
(path / "tests").mkdir()
(path / "config").mkdir()
# Create sample files
(path / "README.md").write_text("""# Test Project
A sample project for testing the session-handoff skill.
## Features
- User authentication
- API endpoints
- Database integration
""")
(path / "src" / "index.js").write_text("""// Main entry point
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('Hello World');
});
module.exports = app;
""")
(path / "src" / "auth.js").write_text("""// Authentication module
const jwt = require('jsonwebtoken');
function validateToken(token) {
// TODO: Implement token validation
return true;
}
function generateToken(user) {
return jwt.sign({ id: user.id }, process.env.JWT_SECRET);
}
module.exports = { validateToken, generateToken };
""")
(path / "src" / "database.js").write_text("""// Database connection
const mongoose = require('mongoose');
async function connect() {
await mongoose.connect(process.env.DATABASE_URL);
}
module.exports = { connect };
""")
(path / "tests" / "auth.test.js").write_text("""// Auth tests
describe('Authentication', () => {
test('validates tokens', () => {
expect(true).toBe(true);
});
});
""")
(path / "config" / "default.json").write_text("""{
"port": 3000,
"database": {
"host": "localhost",
"name": "testdb"
}
}
""")
(path / "package.json").write_text("""{
"name": "test-project",
"version": "1.0.0",
"main": "src/index.js",
"scripts": {
"start": "node src/index.js",
"test": "jest"
}
}
""")
print(f"Created project structure at {path}")
return path
def init_git_repo(path: Path):
"""Initialize git repo with commit history."""
# Initialize
run_cmd(["git", "init"], cwd=str(path))
run_cmd(["git", "config", "user.email", "test@example.com"], cwd=str(path))
run_cmd(["git", "config", "user.name", "Test User"], cwd=str(path))
# Initial commit
run_cmd(["git", "add", "."], cwd=str(path))
run_cmd(["git", "commit", "-m", "Initial commit: project setup"], cwd=str(path))
# Add more commits to simulate history
commits = [
("src/auth.js", "// Added validation logic\n", "Add token validation"),
("src/database.js", "// Added connection pooling\n", "Implement connection pooling"),
("tests/auth.test.js", "// More tests\n", "Add authentication tests"),
("src/index.js", "// Added middleware\n", "Add auth middleware"),
("README.md", "\n## API Docs\n", "Update documentation"),
]
for file, content, message in commits:
file_path = path / file
with open(file_path, "a") as f:
f.write(content)
run_cmd(["git", "add", file], cwd=str(path))
run_cmd(["git", "commit", "-m", message], cwd=str(path))
print(f"Initialized git repo with {len(commits) + 1} commits")
def create_sample_handoffs(path: Path):
"""Create sample handoff documents for testing."""
handoffs_dir = path / ".claude" / "handoffs"
handoffs_dir.mkdir(parents=True)
# Fresh handoff (today)
now = datetime.now()
fresh_name = now.strftime("%Y-%m-%d-%H%M%S") + "-auth-implementation.md"
fresh_content = f"""# Handoff: Implementing User Authentication
## Session Metadata
- Created: {now.strftime("%Y-%m-%d %H:%M:%S")}
- Project: {path}
- Branch: main
- Session duration: 2 hours
## Handoff Chain
- **Continues from**: None (fresh start)
- **Supersedes**: None
## Current State Summary
Working on implementing JWT-based authentication for the API. Successfully added token generation and basic validation. The middleware integration is partially complete.
## Codebase Understanding
### Architecture Overview
Express.js application with modular structure. Auth logic separated into src/auth.js, database connection in src/database.js.
### Critical Files
| File | Purpose | Relevance |
|------|---------|-----------|
| src/auth.js | Authentication logic | Main file being modified |
| src/index.js | App entry point | Needs middleware integration |
### Key Patterns Discovered
- Using environment variables for secrets (JWT_SECRET, DATABASE_URL)
- Jest for testing
## Work Completed
### Tasks Finished
- [x] Set up JWT token generation
- [x] Create basic validation function
- [ ] Integrate middleware (in progress)
### Files Modified
| File | Changes | Rationale |
|------|---------|-----------|
| src/auth.js | Added validateToken, generateToken | Core auth functionality |
### Decisions Made
| Decision | Options Considered | Rationale |
|----------|-------------------|-----------|
| Use JWT over sessions | JWT, Sessions, OAuth | Stateless, scales better for API |
## Pending Work
### Immediate Next Steps
1. Complete middleware integration in src/index.js
2. Add refresh token logic
3. Write comprehensive tests
### Blockers/Open Questions
- [ ] Need to decide on token expiry time (1h vs 24h)
### Deferred Items
- OAuth integration (future sprint)
## Context for Resuming Agent
### Important Context
The validateToken function in src/auth.js currently returns true always - this is a placeholder that needs real implementation. The JWT_SECRET env var must be set.
### Assumptions Made
- Using HS256 algorithm for JWT
- Tokens should be passed in Authorization header
### Potential Gotchas
- Don't forget to set JWT_SECRET environment variable
- Database connection must be established before auth checks
## Environment State
### Tools/Services Used
- Node.js with Express
- JWT library (jsonwebtoken)
### Active Processes
- None currently running
### Environment Variables
- JWT_SECRET
- DATABASE_URL
## Related Resources
- JWT documentation: https://jwt.io
- Express middleware guide
"""
(handoffs_dir / fresh_name).write_text(fresh_content)
# Stale handoff (2 weeks ago)
old_date = now - timedelta(days=14)
stale_name = old_date.strftime("%Y-%m-%d-%H%M%S") + "-database-setup.md"
stale_content = f"""# Handoff: Database Setup
## Session Metadata
- Created: {old_date.strftime("%Y-%m-%d %H:%M:%S")}
- Project: {path}
- Branch: main
- Session duration: 1 hour
## Handoff Chain
- **Continues from**: None (fresh start)
- **Supersedes**: None
## Current State Summary
Set up initial database connection with MongoDB. Basic schema defined but not fully implemented.
## Codebase Understanding
### Architecture Overview
MongoDB database with Mongoose ODM.
### Critical Files
| File | Purpose | Relevance |
|------|---------|-----------|
| src/database.js | DB connection | Main database file |
| src/old-file.js | Legacy code | Was being refactored |
## Pending Work
### Immediate Next Steps
1. Define user schema
2. Add connection pooling
3. Implement error handling
## Context for Resuming Agent
### Important Context
Using MongoDB Atlas for hosting. Connection string in DATABASE_URL.
### Assumptions Made
- MongoDB version 5.x
- Mongoose 7.x
## Environment State
### Environment Variables
- DATABASE_URL
"""
(handoffs_dir / stale_name).write_text(stale_content)
# Incomplete handoff (with TODOs)
incomplete_name = now.strftime("%Y-%m-%d-%H%M%S") + "-incomplete-test.md"
incomplete_content = f"""# Handoff: [TASK_TITLE - replace this]
## Session Metadata
- Created: {now.strftime("%Y-%m-%d %H:%M:%S")}
- Project: {path}
- Branch: main
- Session duration: [estimate how long you worked]
## Current State Summary
[TODO: Write one paragraph describing what was being worked on]
## Codebase Understanding
### Architecture Overview
[TODO: Document key architectural insights]
## Pending Work
### Immediate Next Steps
1. [TODO: Most critical next action]
2. [TODO: Second priority]
## Context for Resuming Agent
### Important Context
[TODO: This is the MOST IMPORTANT section]
"""
(handoffs_dir / incomplete_name).write_text(incomplete_content)
print(f"Created 3 sample handoffs:")
print(f" - {fresh_name} (fresh)")
print(f" - {stale_name} (stale, 14 days old)")
print(f" - {incomplete_name} (incomplete, has TODOs)")
def clean_test_env(path: str):
"""Remove test environment."""
if Path(path).exists():
shutil.rmtree(path)
print(f"Cleaned up test environment at {path}")
else:
print(f"No test environment found at {path}")
def main():
parser = argparse.ArgumentParser(
description="Set up test environment for session-handoff skill"
)
parser.add_argument(
"--path",
default=DEFAULT_TEST_PATH,
help=f"Path for test project (default: {DEFAULT_TEST_PATH})"
)
parser.add_argument(
"--clean",
action="store_true",
help="Remove test environment instead of creating"
)
args = parser.parse_args()
if args.clean:
clean_test_env(args.path)
else:
path = create_test_project(args.path)
init_git_repo(path)
create_sample_handoffs(path)
print(f"\nTest environment ready at: {args.path}")
print(f"\nTo test, run:")
print(f" cd {args.path}")
print(f" # Then use Claude Code with the session-handoff skill")
if __name__ == "__main__":
main()
Evaluation Test Scenarios
Test these scenarios across Haiku, Sonnet, and Opus to verify skill effectiveness.
Test Setup
Before running tests: 1. Create a test project directory with some files 2. Initialize git repository 3. Make some commits 4. Have the skill available
# Setup test environment
mkdir -p /tmp/handoff-test-project/src
cd /tmp/handoff-test-project
git init
echo "console.log('hello');" > src/index.js
echo "# Test Project" > README.md
git add . && git commit -m "Initial commit"
echo "function test() {}" >> src/index.js
git add . && git commit -m "Add test function"---
Scenario 1: Basic Handoff Creation
Trigger phrase: "create a handoff"
User prompt:
I've been working on implementing user authentication. I modified src/auth.js
to add JWT token validation and updated the middleware. Create a handoff so
I can continue later.Expected behavior:
- [ ] Skill triggers (recognizes "create a handoff")
- [ ] Runs
create_handoff.pyscript - [ ] Creates file in
.claude/handoffs/ - [ ] Pre-fills metadata (timestamp, project path, git branch)
- [ ] Prompts user to complete TODO sections
- [ ] Mentions validation step
Model-specific expectations:
| Model | Expectation |
|---|---|
| Haiku | Should follow script instructions literally, may need prompting for details |
| Sonnet | Should execute workflow smoothly, fill in reasonable context |
| Opus | Should proactively add rich context, may suggest improvements |
---
Scenario 2: Handoff with Chaining
Trigger phrase: "continue from previous handoff"
Setup: First create a handoff using Scenario 1, then:
User prompt:
I'm continuing the auth work from yesterday. Create a new handoff that
links to the previous one.Expected behavior:
- [ ] Lists existing handoffs
- [ ] Uses
--continues-fromflag - [ ] Adds "Handoff Chain" section with link
- [ ] References previous handoff context
---
Scenario 3: Resume from Handoff
Trigger phrase: "resume from handoff" or "load handoff"
Setup: Have an existing handoff file
User prompt:
I want to continue where I left off. Load my last handoff.Expected behavior:
- [ ] Runs
list_handoffs.pyto find handoffs - [ ] Runs
check_staleness.pyon selected handoff - [ ] Reports staleness level
- [ ] Reads handoff document
- [ ] Summarizes "Immediate Next Steps"
- [ ] Follows resume checklist
---
Scenario 4: Proactive Handoff Suggestion
Trigger: After substantial work (no explicit request)
Setup: Simulate a long session by describing significant work done
User prompt:
Great, we've now:
1. Refactored the database connection pooling
2. Fixed the N+1 query in UserService
3. Added caching layer with Redis
4. Updated all the tests
5. Fixed 3 TypeScript errors
What's next?Expected behavior:
- [ ] Recognizes substantial work done (5+ items)
- [ ] Proactively suggests creating a handoff
- [ ] Uses the suggested phrasing from SKILL.md
---
Scenario 5: Validation Flow
Trigger phrase: "validate the handoff"
Setup: Create a handoff with incomplete sections
User prompt:
I created a handoff but I'm not sure if it's complete. Can you validate it?Expected behavior:
- [ ] Runs
validate_handoff.py - [ ] Reports quality score
- [ ] Lists missing/incomplete sections
- [ ] Warns about any secrets detected
- [ ] Provides actionable next steps
---
Scenario 6: Staleness Check
Trigger phrase: "check if handoff is still valid"
Setup: Have an older handoff with several commits since
User prompt:
I have a handoff from last week. Is it still relevant or should I
create a new one?Expected behavior:
- [ ] Runs
check_staleness.py - [ ] Reports staleness level (FRESH/SLIGHTLY_STALE/STALE/VERY_STALE)
- [ ] Lists specific issues (days old, commits since, etc.)
- [ ] Provides recommendation
---
Scenario 7: Secret Detection
Trigger: During handoff creation with sensitive content
User prompt:
Create a handoff. Note that I configured the API with key sk-abc123xyz
and the database password is "supersecret".Expected behavior:
- [ ] Creates handoff
- [ ] Runs validation
- [ ] Detects potential secrets
- [ ] Warns user about security risk
- [ ] Recommends removing sensitive data
---
Scoring Rubric
For each scenario, score:
| Criterion | Points | Description |
|---|---|---|
| Triggers correctly | 2 | Skill activates on trigger phrase |
| Follows workflow | 3 | Executes steps in correct order |
| Uses scripts | 2 | Runs appropriate Python scripts |
| Output quality | 2 | Produces useful, accurate output |
| Error handling | 1 | Handles edge cases gracefully |
| Total | 10 | Per scenario |
Pass threshold: 7/10 per scenario
---
Results Template
## Test Results: [Model Name]
Date: YYYY-MM-DD
Model: [haiku/sonnet/opus]
Skill version: session-handoff
| Scenario | Score | Notes |
|----------|-------|-------|
| 1. Basic Creation | /10 | |
| 2. Chaining | /10 | |
| 3. Resume | /10 | |
| 4. Proactive | /10 | |
| 5. Validation | /10 | |
| 6. Staleness | /10 | |
| 7. Secret Detection | /10 | |
| **Total** | /70 | |
### Issues Found
-
### Recommendations
-Session Handoff Skill
Overview
The Session Handoff skill creates comprehensive handoff documents that enable fresh AI agents to seamlessly continue work with zero ambiguity. It solves the long-running agent context exhaustion problem by preserving complete context, decisions, and state across sessions.
Purpose
When working on complex, multi-session projects with AI agents, context gets lost between sessions. This skill provides a structured approach to:
- Preserve context - Capture all critical information before context window fills
- Enable continuity - Allow new agents to pick up exactly where you left off
- Document decisions - Record architectural choices and their rationale
- Track progress - Maintain clear status of completed and pending work
- Chain sessions - Link related handoffs for long-running projects
When to Use
User-Triggered
- User says "save state", "create handoff", "I need to pause"
- User requests "load handoff", "resume from", "continue where we left off"
- User mentions "context is getting full" or "save this for later"
Agent-Triggered (Proactive)
- Context window approaching capacity (>80% full)
- Major task milestone completed
- Work session ending with significant progress
- After substantial work (5+ file edits, complex debugging, architecture decisions)
- Before switching to a different task
Resumption Scenarios
- Starting a new session on an existing project
- Different agent needs to continue the work
- Need to recall decisions made in previous sessions
- Picking up after a long break
How It Works
The skill operates in two primary modes:
CREATE Mode
Generates a comprehensive handoff document capturing current state:
1. Generate Scaffold - Smart script pre-fills metadata (timestamp, git status, modified files) 2. Complete Document - Fill in critical sections (state, context, decisions, next steps) 3. Validate - Automated checks for completeness, quality, and security 4. Confirm - Present location and summary to user
RESUME Mode
Loads and validates existing handoff documents:
1. Find Handoffs - List available handoffs in project 2. Check Staleness - Assess if context is still current 3. Load Document - Read handoff (and chain if linked) 4. Verify Context - Validate assumptions and environment 5. Begin Work - Start from "Immediate Next Steps"
Key Features
Smart Scaffolding
The create_handoff.py script automatically captures:
- Timestamp and project path
- Current git branch and recent commits
- Modified and unstaged files
- Handoff chain links (if continuing from previous)
Validation & Quality Assurance
The validate_handoff.py script checks:
- No incomplete
[TODO: ...]placeholders - All required sections populated
- No potential secrets (API keys, passwords, tokens)
- Referenced files exist
- Quality score (0-100)
Staleness Detection
The check_staleness.py script assesses:
- Time elapsed since handoff creation
- Git commits made since handoff
- Files changed since handoff
- Branch divergence
- Missing referenced files
Handoff Chaining
For long-running projects, chain handoffs together:
handoff-1.md (initial work)
↓
handoff-2.md --continues-from handoff-1.md
↓
handoff-3.md --continues-from handoff-2.mdEach handoff links to its predecessor, providing context breadcrumbs for new agents.
Usage Examples
Creating a Handoff
Basic handoff creation:
python scripts/create_handoff.py implementing-user-authContinuation handoff (linked to previous):
python scripts/create_handoff.py "auth-part-2" --continues-from 2024-01-15-auth.mdValidate before finalizing:
python scripts/validate_handoff.py .claude/handoffs/2024-01-15-143022-implementing-auth.mdResuming from a Handoff
List available handoffs:
python scripts/list_handoffs.pyCheck if handoff is current:
python scripts/check_staleness.py .claude/handoffs/2024-01-15-143022-implementing-auth.mdLoad and continue work: 1. Read the handoff document completely 2. Verify context using resume checklist 3. Start with first item in "Immediate Next Steps"
Handoff Document Structure
A complete handoff includes:
1. Metadata - Timestamp, project path, git branch, commits 2. Current State Summary - What's happening right now 3. Important Context - Critical information for next agent 4. Decisions Made - Architectural choices with rationale 5. Immediate Next Steps - Clear, actionable first steps 6. Pending Work - Remaining tasks and priorities 7. Critical Files - Important locations and their purpose 8. Key Patterns Discovered - Conventions and approaches 9. Potential Gotchas - Known issues and workarounds 10. Handoff Chain - Links to previous/next handoffs
See references/handoff-template.md for the complete template.
Storage Location
Handoffs are stored in: .claude/handoffs/
Naming convention: YYYY-MM-DD-HHMMSS-[slug].md
Example: 2024-01-15-143022-implementing-auth.md
Scripts Reference
| Script | Purpose | Usage |
|---|---|---|
create_handoff.py | Generate new handoff with smart scaffolding | python scripts/create_handoff.py [slug] [--continues-from <file>] |
list_handoffs.py | List available handoffs in a project | python scripts/list_handoffs.py [path] |
validate_handoff.py | Check completeness, quality, and security | python scripts/validate_handoff.py <file> |
check_staleness.py | Assess if handoff context is still current | python scripts/check_staleness.py <file> |
Quality Standards
Do not finalize a handoff if:
- Validation score is below 70
- Secrets are detected
[TODO: ...]placeholders remain- Required sections are empty
Best practices:
- Write clear, specific next steps (not vague goals)
- Document the "why" behind decisions, not just the "what"
- Include code snippets for critical patterns
- Reference specific file paths and line numbers
- Update handoffs as work progresses
References
- handoff-template.md - Complete template structure with guidance
- resume-checklist.md - Verification checklist for resuming agents
- evals/model-expectations.md - Model behavior expectations
- evals/test-scenarios.md - Test cases for handoff creation and resumption
Benefits
- Zero ambiguity - New agents know exactly what to do
- Context preservation - No loss of critical information
- Decision history - Understand why choices were made
- Reduced onboarding - Faster agent startup on existing work
- Quality assurance - Automated validation prevents incomplete handoffs
- Security - Secret detection prevents credential leaks
- Long-term memory - Handoff chains maintain project history
Handoff Template
Use this template structure when creating handoff documents. The smart scaffold script will pre-fill metadata sections; complete the remaining sections based on session context.
Table of Contents
- Session Metadata
- Current State Summary
- Codebase Understanding
- Architecture Overview
- Critical Files
- Key Patterns Discovered
- Work Completed
- Tasks Finished
- Files Modified
- Decisions Made
- Pending Work
- Immediate Next Steps
- Blockers/Open Questions
- Deferred Items
- Context for Resuming Agent
- Important Context
- Assumptions Made
- Potential Gotchas
- Environment State
- Related Resources
- Template Usage Notes
---
Handoff: [TASK_TITLE]
Session Metadata
- Created: [TIMESTAMP]
- Project: [PROJECT_PATH]
- Branch: [GIT_BRANCH]
- Session duration: [APPROX_DURATION]
Current State Summary
[One paragraph: What was being worked on, current status, and where things left off]
Codebase Understanding
Architecture Overview
[Key architectural insights discovered during this session - how the system is structured, main components, data flow]
Critical Files
| File | Purpose | Relevance |
|---|---|---|
| path/to/file | What this file does | Why it matters for this task |
Key Patterns Discovered
[Important patterns, conventions, or idioms found in this codebase that the next agent should follow]
Work Completed
Tasks Finished
- [x] Task 1 - brief description of what was done
- [x] Task 2 - brief description
Files Modified
| File | Changes | Rationale |
|---|---|---|
| path/to/file | Description of changes | Why this change was made |
Decisions Made
| Decision | Options Considered | Rationale |
|---|---|---|
| Chose X over Y | X, Y, Z | Why X was chosen |
Pending Work
Immediate Next Steps
1. [Most critical next action - what to do first] 2. [Second priority] 3. [Third priority]
Blockers/Open Questions
- [ ] Blocker: [description] - Needs: [what's required to unblock]
- [ ] Question: [unclear aspect] - Suggested: [potential resolution]
Deferred Items
- Item 1 (deferred because: [reason, e.g., out of scope, needs user input])
Context for Resuming Agent
Important Context
[Critical information the next agent MUST know to continue effectively - this is the most important section for handoff]
Assumptions Made
- Assumption 1: [what was assumed to be true]
- Assumption 2: [another assumption]
Potential Gotchas
- [Things that might trip up a new agent - edge cases, quirks, non-obvious behavior]
Environment State
Tools/Services Used
- [Tool/Service]: [relevant configuration or state]
Active Processes
- [Any background processes, dev servers, watchers that may be running]
Environment Variables
- [Key env vars that matter for this work - DO NOT include secrets/values, just names]
Related Resources
- [Link to relevant documentation]
- [Related file paths]
- [External resources consulted]
---
Template Usage Notes
When filling this template: 1. Be specific and concrete - vague descriptions don't help the next agent 2. Include file paths with line numbers where relevant (e.g., src/auth.ts:142) 3. Prioritize the "Important Context" and "Immediate Next Steps" sections 4. Don't include sensitive data (API keys, passwords, tokens) 5. Focus on WHAT and WHY, not just WHAT - rationale is crucial for handoffs
Resume Checklist
Follow this checklist when resuming work from a handoff document to ensure zero-ambiguity continuation.
Pre-Resume Verification
- [ ] Read the entire handoff document before taking any action
- [ ] Verify you are in the correct project directory
- [ ] Confirm the git branch matches (or understand why it might differ)
- [ ] Check the handoff timestamp - how stale is this context?
Context Validation
- [ ] Review "Important Context" section thoroughly
- [ ] Understand all assumptions listed - are they still valid?
- [ ] Check if any blockers have been resolved since handoff
- [ ] Review "Potential Gotchas" to avoid known pitfalls
State Verification
- [ ] Run
git statusto see current file state - [ ] Compare modified files list in handoff vs current state
- [ ] Check if any environment variables need to be set
- [ ] Verify any required services/processes are running
Resume Execution
- [ ] Start with "Immediate Next Steps" item #1
- [ ] Reference "Files Modified" table for context on recent changes
- [ ] Apply patterns documented in "Key Patterns Discovered"
- [ ] Follow architectural insights from "Architecture Overview"
During Work
- [ ] Update handoff document if major new context is discovered
- [ ] Mark completed items in "Pending Work" as you finish them
- [ ] Add new blockers/questions as they arise
- [ ] Consider creating a new handoff if session becomes long
Red Flags - Stop and Verify
If you encounter any of these, pause and verify context before proceeding:
1. Files mentioned in handoff don't exist - codebase may have changed significantly 2. Branch has diverged substantially - check git log for recent commits 3. Assumptions are clearly invalid - reassess the approach 4. Blockers marked as unresolved are now blocking you - escalate to user 5. Architecture has changed - re-explore before continuing
Quick Start Commands
After reading the handoff, these commands help verify state:
# Check current branch and status
git branch --show-current
git status
# See recent commits (compare with handoff)
git log --oneline -10
# Check for any running processes mentioned
ps aux | grep [process-name]
# Verify environment
env | grep [relevant-var]Handoff Quality Assessment
Rate the handoff quality to identify if more exploration is needed:
| Aspect | Good | Needs Exploration |
|---|---|---|
| Next steps | Clear, actionable | Vague or missing |
| File references | Specific paths/lines | General descriptions |
| Decisions | Rationale included | Just outcomes |
| Context | Complete picture | Gaps or assumptions |
If multiple aspects "Need Exploration", spend time re-exploring the codebase before continuing implementation.
#!/usr/bin/env python3
"""
Check staleness of a handoff document compared to current project state.
Analyzes:
- Time since handoff was created
- Git commits since handoff
- Files that changed since handoff
- Branch divergence
- Modified files status
Usage:
python check_staleness.py <handoff-file>
python check_staleness.py .claude/handoffs/2024-01-15-143022-auth.md
"""
import os
import re
import subprocess
import sys
from datetime import datetime
from pathlib import Path
def run_cmd(cmd: list[str], cwd: str = None) -> tuple[bool, str]:
"""Run a command and return (success, output)."""
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
cwd=cwd,
timeout=10
)
return result.returncode == 0, result.stdout.strip()
except (subprocess.TimeoutExpired, FileNotFoundError):
return False, ""
def parse_handoff_metadata(filepath: str) -> dict:
"""Extract metadata from a handoff file."""
content = Path(filepath).read_text()
metadata = {
"created": None,
"branch": None,
"project_path": None,
"modified_files": [],
}
# Parse Created timestamp
match = re.search(r'Created:\s*(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})', content)
if match:
try:
metadata["created"] = datetime.strptime(match.group(1), "%Y-%m-%d %H:%M:%S")
except ValueError:
pass
# Parse Branch
match = re.search(r'Branch:\s*(\S+)', content)
if match:
branch = match.group(1)
if branch and not branch.startswith('['):
metadata["branch"] = branch
# Parse Project path
match = re.search(r'Project:\s*(.+?)(?:\n|$)', content)
if match:
metadata["project_path"] = match.group(1).strip()
# Parse modified files from table
table_matches = re.findall(r'\|\s*([a-zA-Z0-9_\-./]+\.[a-zA-Z]+)\s*\|', content)
for f in table_matches:
if '/' in f and not f.startswith('['):
metadata["modified_files"].append(f)
return metadata
def get_commits_since(timestamp: datetime, project_path: str) -> list[str]:
"""Get list of commits since a given timestamp."""
if not timestamp:
return []
iso_time = timestamp.strftime("%Y-%m-%dT%H:%M:%S")
success, output = run_cmd(
["git", "log", f"--since={iso_time}", "--oneline", "--no-decorate"],
cwd=project_path
)
if success and output:
return output.split("\n")
return []
def get_current_branch(project_path: str) -> str | None:
"""Get current git branch."""
success, branch = run_cmd(
["git", "branch", "--show-current"],
cwd=project_path
)
return branch if success else None
def get_changed_files_since(timestamp: datetime, project_path: str) -> list[str]:
"""Get files that changed since timestamp."""
if not timestamp:
return []
iso_time = timestamp.strftime("%Y-%m-%dT%H:%M:%S")
success, output = run_cmd(
["git", "diff", "--name-only", f"--since={iso_time}", "HEAD"],
cwd=project_path
)
# Fallback: get files changed in commits since timestamp
if not output:
success, output = run_cmd(
["git", "log", f"--since={iso_time}", "--name-only", "--pretty=format:"],
cwd=project_path
)
if success and output:
files = [f.strip() for f in output.split("\n") if f.strip()]
return list(set(files)) # Deduplicate
return []
def check_files_exist(files: list[str], project_path: str) -> tuple[list[str], list[str]]:
"""Check which files from handoff still exist."""
existing = []
missing = []
for f in files:
full_path = Path(project_path) / f
if full_path.exists():
existing.append(f)
else:
missing.append(f)
return existing, missing
def calculate_staleness_level(
days_old: float,
commits_since: int,
files_changed: int,
branch_matches: bool,
files_missing: int
) -> tuple[str, str, list[str]]:
"""Calculate staleness level and provide recommendations.
Staleness scoring rationale:
- Each factor adds 1-3 points based on severity
- Thresholds based on typical development patterns:
- Age: 1 day (active work), 7 days (sprint), 30 days (stale)
- Commits: 5 (minor changes), 20 (feature work), 50 (major changes)
- Files: 5 (localized), 20 (widespread changes)
- Final score: 0=FRESH, 1-2=SLIGHTLY_STALE, 3-4=STALE, 5+=VERY_STALE
"""
issues = []
# Scoring
staleness_score = 0
# Age thresholds: 1 day = active, 7 days = sprint boundary, 30 days = likely stale
if days_old > 30:
staleness_score += 3 # Over a month: high risk of outdated context
issues.append(f"Handoff is {int(days_old)} days old")
elif days_old > 7:
staleness_score += 2 # Over a week: moderate staleness
issues.append(f"Handoff is {int(days_old)} days old")
elif days_old > 1:
staleness_score += 1 # Over a day: minor staleness
# Commit thresholds: 5 = routine, 20 = feature work, 50 = major development
if commits_since > 50:
staleness_score += 3 # Major changes likely invalidate handoff context
issues.append(f"{commits_since} commits since handoff - significant changes")
elif commits_since > 20:
staleness_score += 2 # Substantial work done since handoff
issues.append(f"{commits_since} commits since handoff")
elif commits_since > 5:
staleness_score += 1 # Some changes, worth reviewing
# Branch mismatch: likely working on different feature/context
if not branch_matches:
staleness_score += 2 # Different branch = different context
issues.append("Current branch differs from handoff branch")
# Missing files: 5+ suggests significant restructuring
if files_missing > 5:
staleness_score += 2 # Many refs broken = codebase restructured
issues.append(f"{files_missing} referenced files no longer exist")
elif files_missing > 0:
staleness_score += 1 # Some refs broken
issues.append(f"{files_missing} referenced file(s) missing")
# Changed files: 5 = localized, 20 = widespread
if files_changed > 20:
staleness_score += 2 # Widespread changes affect handoff relevance
issues.append(f"{files_changed} files changed since handoff")
elif files_changed > 5:
staleness_score += 1 # Some files changed
# Staleness levels: 0=fresh, 1-2=slight, 3-4=stale, 5+=very stale
if staleness_score == 0:
level = "FRESH"
recommendation = "Safe to resume - minimal changes since handoff"
elif staleness_score <= 2:
level = "SLIGHTLY_STALE"
recommendation = "Generally safe to resume - review changes before continuing"
elif staleness_score <= 4:
level = "STALE"
recommendation = "Proceed with caution - significant changes may affect context"
else:
level = "VERY_STALE"
recommendation = "Consider creating new handoff - too many changes since original"
return level, recommendation, issues
def check_staleness(handoff_path: str) -> dict:
"""Run staleness check on a handoff file."""
path = Path(handoff_path)
if not path.exists():
return {"error": f"Handoff file not found: {handoff_path}"}
# Parse handoff
metadata = parse_handoff_metadata(handoff_path)
# Determine project path
project_path = metadata.get("project_path")
if not project_path or not Path(project_path).exists():
# Fallback: assume handoff is in .claude/handoffs/ within project
project_path = str(path.parent.parent.parent)
# Check if git repo
success, _ = run_cmd(["git", "rev-parse", "--git-dir"], cwd=project_path)
is_git_repo = success
result = {
"handoff_file": str(path),
"project_path": project_path,
"is_git_repo": is_git_repo,
"created": metadata["created"],
"handoff_branch": metadata["branch"],
}
# Calculate age
if metadata["created"]:
age = datetime.now() - metadata["created"]
result["days_old"] = age.total_seconds() / 86400
result["hours_old"] = age.total_seconds() / 3600
else:
result["days_old"] = None
result["hours_old"] = None
if is_git_repo:
# Git-based checks
result["current_branch"] = get_current_branch(project_path)
result["branch_matches"] = (
result["current_branch"] == metadata["branch"]
if metadata["branch"] else True
)
commits = get_commits_since(metadata["created"], project_path)
result["commits_since"] = len(commits)
result["recent_commits"] = commits[:5] # Show first 5
changed_files = get_changed_files_since(metadata["created"], project_path)
result["files_changed_count"] = len(changed_files)
result["files_changed"] = changed_files[:10] # Show first 10
# Check if handoff's modified files still exist
existing, missing = check_files_exist(metadata["modified_files"], project_path)
result["referenced_files_exist"] = len(existing)
result["referenced_files_missing"] = missing
# Calculate staleness
level, recommendation, issues = calculate_staleness_level(
result.get("days_old", 0) or 0,
result["commits_since"],
result["files_changed_count"],
result["branch_matches"],
len(missing)
)
result["staleness_level"] = level
result["recommendation"] = recommendation
result["issues"] = issues
else:
# Non-git checks (limited)
result["staleness_level"] = "UNKNOWN"
result["recommendation"] = "Not a git repo - unable to detect changes"
result["issues"] = ["Project is not a git repository"]
return result
def print_report(result: dict):
"""Print staleness report."""
if "error" in result:
print(f"Error: {result['error']}")
return
print(f"\n{'='*60}")
print(f"Handoff Staleness Report")
print(f"{'='*60}")
print(f"File: {result['handoff_file']}")
print(f"Project: {result['project_path']}")
if result["created"]:
print(f"Created: {result['created'].strftime('%Y-%m-%d %H:%M:%S')}")
if result["days_old"] is not None:
if result["days_old"] < 1:
print(f"Age: {result['hours_old']:.1f} hours")
else:
print(f"Age: {result['days_old']:.1f} days")
print(f"\n{'='*60}")
print(f"Staleness Level: {result['staleness_level']}")
print(f"{'='*60}")
print(f"\nRecommendation: {result['recommendation']}")
if result.get("issues"):
print(f"\nIssues detected:")
for issue in result["issues"]:
print(f" - {issue}")
if result.get("is_git_repo"):
print(f"\n--- Git Status ---")
print(f"Handoff branch: {result.get('handoff_branch', 'Unknown')}")
print(f"Current branch: {result.get('current_branch', 'Unknown')}")
print(f"Branch matches: {'Yes' if result.get('branch_matches') else 'No'}")
print(f"Commits since handoff: {result.get('commits_since', 0)}")
print(f"Files changed: {result.get('files_changed_count', 0)}")
if result.get("recent_commits"):
print(f"\nRecent commits:")
for commit in result["recent_commits"][:5]:
print(f" {commit}")
if result.get("referenced_files_missing"):
print(f"\nMissing referenced files:")
for f in result["referenced_files_missing"][:5]:
print(f" - {f}")
print(f"\n{'='*60}")
# Color-coded verdict (using text indicators)
level = result.get("staleness_level", "UNKNOWN")
if level == "FRESH":
print("Verdict: [OK] Safe to resume")
elif level == "SLIGHTLY_STALE":
print("Verdict: [OK] Review changes, then resume")
elif level == "STALE":
print("Verdict: [CAUTION] Verify context before resuming")
elif level == "VERY_STALE":
print("Verdict: [WARNING] Consider creating fresh handoff")
else:
print("Verdict: [UNKNOWN] Manual verification needed")
def main():
if len(sys.argv) < 2:
print("Usage: python check_staleness.py <handoff-file>")
print("Example: python check_staleness.py .claude/handoffs/2024-01-15-auth.md")
sys.exit(1)
handoff_path = sys.argv[1]
result = check_staleness(handoff_path)
print_report(result)
# Exit code based on staleness
level = result.get("staleness_level", "UNKNOWN")
if level in ["FRESH", "SLIGHTLY_STALE"]:
sys.exit(0)
elif level == "STALE":
sys.exit(1)
else:
sys.exit(2)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Smart scaffold generator for handoff documents.
Creates a new handoff document with auto-detected metadata:
- Current timestamp
- Project path
- Git branch (if available)
- Recent git log
- Modified/staged files
- Handoff chain linking
Usage:
python create_handoff.py [task-slug] [--continues-from <previous-handoff>]
python create_handoff.py "implementing-auth"
python create_handoff.py "auth-part-2" --continues-from 2024-01-15-auth.md
python create_handoff.py # auto-generates slug from timestamp
"""
import argparse
import os
import re
import subprocess
import sys
from datetime import datetime
from pathlib import Path
def run_cmd(cmd: list[str], cwd: str = None) -> tuple[bool, str]:
"""Run a command and return (success, output)."""
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
cwd=cwd,
timeout=10
)
return result.returncode == 0, result.stdout.strip()
except (subprocess.TimeoutExpired, FileNotFoundError):
return False, ""
def get_git_info(project_path: str) -> dict:
"""Gather git information from the project."""
info = {
"is_git_repo": False,
"branch": None,
"recent_commits": [],
"modified_files": [],
"staged_files": [],
}
# Check if git repo
success, _ = run_cmd(["git", "rev-parse", "--git-dir"], cwd=project_path)
if not success:
return info
info["is_git_repo"] = True
# Get current branch
success, branch = run_cmd(["git", "branch", "--show-current"], cwd=project_path)
if success and branch:
info["branch"] = branch
# Get recent commits (last 5)
success, log = run_cmd(
["git", "log", "--oneline", "-5", "--no-decorate"],
cwd=project_path
)
if success and log:
info["recent_commits"] = log.split("\n")
# Get modified files (unstaged)
success, modified = run_cmd(
["git", "diff", "--name-only"],
cwd=project_path
)
if success and modified:
info["modified_files"] = modified.split("\n")
# Get staged files
success, staged = run_cmd(
["git", "diff", "--name-only", "--cached"],
cwd=project_path
)
if success and staged:
info["staged_files"] = staged.split("\n")
return info
def find_previous_handoffs(project_path: str) -> list[dict]:
"""Find existing handoffs in the project."""
handoffs_dir = Path(project_path) / ".claude" / "handoffs"
if not handoffs_dir.exists():
return []
handoffs = []
for filepath in handoffs_dir.glob("*.md"):
# Extract title from file
try:
content = filepath.read_text()
match = re.search(r'^#\s+(?:Handoff:\s*)?(.+)$', content, re.MULTILINE)
title = match.group(1).strip() if match else filepath.stem
except Exception:
title = filepath.stem
# Parse date from filename
date_match = re.match(r'(\d{4}-\d{2}-\d{2})-(\d{6})', filepath.name)
if date_match:
try:
date = datetime.strptime(
f"{date_match.group(1)} {date_match.group(2)}",
"%Y-%m-%d %H%M%S"
)
except ValueError:
date = None
else:
date = None
handoffs.append({
"filename": filepath.name,
"path": str(filepath),
"title": title,
"date": date,
})
# Sort by date, most recent first
handoffs.sort(key=lambda x: x["date"] or datetime.min, reverse=True)
return handoffs
def get_previous_handoff_info(project_path: str, continues_from: str = None) -> dict:
"""Get information about the previous handoff for chaining."""
handoffs = find_previous_handoffs(project_path)
if continues_from:
# Find specific handoff
for h in handoffs:
if continues_from in h["filename"]:
return {
"exists": True,
"filename": h["filename"],
"title": h["title"],
}
return {"exists": False, "filename": continues_from, "title": "Not found"}
elif handoffs:
# Suggest most recent
most_recent = handoffs[0]
return {
"exists": True,
"filename": most_recent["filename"],
"title": most_recent["title"],
"suggested": True,
}
return {"exists": False}
def generate_handoff(
project_path: str,
slug: str = None,
continues_from: str = None
) -> str:
"""Generate a handoff document with pre-filled metadata."""
# Generate timestamp and filename
now = datetime.now()
timestamp = now.strftime("%Y-%m-%d %H:%M:%S")
file_timestamp = now.strftime("%Y-%m-%d-%H%M%S")
if not slug:
slug = "handoff"
# Sanitize slug
slug = slug.lower().replace(" ", "-").replace("_", "-")
slug = "".join(c for c in slug if c.isalnum() or c == "-")
filename = f"{file_timestamp}-{slug}.md"
# Create handoffs directory
handoffs_dir = Path(project_path) / ".claude" / "handoffs"
handoffs_dir.mkdir(parents=True, exist_ok=True)
filepath = handoffs_dir / filename
# Gather git info
git_info = get_git_info(project_path)
# Get previous handoff info for chaining
prev_handoff = get_previous_handoff_info(project_path, continues_from)
# Build pre-filled sections
branch_line = git_info["branch"] if git_info["branch"] else "[not a git repo or detached HEAD]"
# Recent commits section
if git_info["recent_commits"]:
commits_section = "\n".join(f" - {c}" for c in git_info["recent_commits"])
else:
commits_section = " - [no recent commits or not a git repo]"
# Modified files section
all_modified = list(set(git_info["modified_files"] + git_info["staged_files"]))
if all_modified:
modified_section = "\n".join(f"| {f} | [describe changes] | [why changed] |" for f in all_modified[:10])
if len(all_modified) > 10:
modified_section += f"\n| ... and {len(all_modified) - 10} more files | | |"
else:
modified_section = "| [no modified files detected] | | |"
# Handoff chain section
if prev_handoff.get("exists"):
chain_section = f"""## Handoff Chain
- **Continues from**: [{prev_handoff['filename']}](./{prev_handoff['filename']})
- Previous title: {prev_handoff.get('title', 'Unknown')}
- **Supersedes**: [list any older handoffs this replaces, or "None"]
> Review the previous handoff for full context before filling this one."""
else:
chain_section = """## Handoff Chain
- **Continues from**: None (fresh start)
- **Supersedes**: None
> This is the first handoff for this task."""
# Generate the document
content = f"""# Handoff: [TASK_TITLE - replace this]
## Session Metadata
- Created: {timestamp}
- Project: {project_path}
- Branch: {branch_line}
- Session duration: [estimate how long you worked]
### Recent Commits (for context)
{commits_section}
{chain_section}
## Current State Summary
[TODO: Write one paragraph describing what was being worked on, current status, and where things left off]
## Codebase Understanding
### Architecture Overview
[TODO: Document key architectural insights discovered during this session]
### Critical Files
| File | Purpose | Relevance |
|------|---------|-----------|
| [TODO: Add critical files] | | |
### Key Patterns Discovered
[TODO: Document important patterns, conventions, or idioms found in this codebase]
## Work Completed
### Tasks Finished
- [ ] [TODO: List completed tasks]
### Files Modified
| File | Changes | Rationale |
|------|---------|-----------|
{modified_section}
### Decisions Made
| Decision | Options Considered | Rationale |
|----------|-------------------|-----------|
| [TODO: Document key decisions] | | |
## Pending Work
### Immediate Next Steps
1. [TODO: Most critical next action]
2. [TODO: Second priority]
3. [TODO: Third priority]
### Blockers/Open Questions
- [ ] [TODO: List any blockers or open questions]
### Deferred Items
- [TODO: Items deferred and why]
## Context for Resuming Agent
### Important Context
[TODO: This is the MOST IMPORTANT section - write critical information the next agent MUST know]
### Assumptions Made
- [TODO: List assumptions made during this session]
### Potential Gotchas
- [TODO: Document things that might trip up a new agent]
## Environment State
### Tools/Services Used
- [TODO: List relevant tools and their configuration]
### Active Processes
- [TODO: Note any running processes, servers, etc.]
### Environment Variables
- [TODO: List relevant env var NAMES only - NEVER include actual values/secrets]
## Related Resources
- [TODO: Add links to relevant docs and files]
---
**Security Reminder**: Before finalizing, run `validate_handoff.py` to check for accidental secret exposure.
"""
# Write the file
filepath.write_text(content)
return str(filepath)
def main():
parser = argparse.ArgumentParser(
description="Create a new handoff document with smart scaffolding"
)
parser.add_argument(
"slug",
nargs="?",
default=None,
help="Short identifier for the handoff (e.g., 'implementing-auth')"
)
parser.add_argument(
"--continues-from",
dest="continues_from",
help="Filename of previous handoff this continues from"
)
args = parser.parse_args()
# Get project path (current working directory)
project_path = os.getcwd()
# Check for existing handoffs to suggest chaining
if not args.continues_from:
prev_handoffs = find_previous_handoffs(project_path)
if prev_handoffs:
print(f"Found {len(prev_handoffs)} existing handoff(s).")
print(f"Most recent: {prev_handoffs[0]['filename']}")
print(f"Use --continues-from <filename> to link handoffs.\n")
# Generate handoff
filepath = generate_handoff(project_path, args.slug, args.continues_from)
print(f"Created handoff document: {filepath}")
print(f"\nNext steps:")
print(f"1. Open {filepath}")
print(f"2. Replace [TODO: ...] placeholders with actual content")
print(f"3. Focus especially on 'Important Context' and 'Immediate Next Steps'")
print(f"4. Run: python validate_handoff.py {filepath}")
print(f" (Checks for completeness and accidental secrets)")
return filepath
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
List available handoff documents in the current project.
Searches for handoff documents in .claude/handoffs/ and displays:
- Filename with date
- Title extracted from document
- Status (if marked complete)
Usage:
python list_handoffs.py # List handoffs in current project
python list_handoffs.py /path # List handoffs in specified path
"""
import os
import re
import sys
from datetime import datetime
from pathlib import Path
def extract_title(filepath: Path) -> str:
"""Extract the title from a handoff document."""
try:
content = filepath.read_text()
# Look for first H1 header
match = re.search(r'^#\s+(?:Handoff:\s*)?(.+)$', content, re.MULTILINE)
if match:
title = match.group(1).strip()
# Clean up placeholder text
if title.startswith("[") and title.endswith("]"):
return "[Untitled - needs completion]"
return title[:50] + "..." if len(title) > 50 else title
except Exception:
pass
return "[Unable to read title]"
def check_completion_status(filepath: Path) -> str:
"""Check if handoff appears complete or has TODOs remaining."""
try:
content = filepath.read_text()
todo_count = content.count("[TODO:")
if todo_count == 0:
return "Complete"
elif todo_count <= 3:
return f"In Progress ({todo_count} TODOs)"
else:
return f"Needs Work ({todo_count} TODOs)"
except Exception:
return "Unknown"
def parse_date_from_filename(filename: str) -> datetime | None:
"""Extract date from filename like 2024-01-15-143022-slug.md"""
match = re.match(r'(\d{4}-\d{2}-\d{2})-(\d{6})', filename)
if match:
try:
date_str = match.group(1)
time_str = match.group(2)
return datetime.strptime(f"{date_str} {time_str}", "%Y-%m-%d %H%M%S")
except ValueError:
pass
return None
def list_handoffs(project_path: str) -> list[dict]:
"""List all handoff documents in a project."""
handoffs_dir = Path(project_path) / ".claude" / "handoffs"
if not handoffs_dir.exists():
return []
handoffs = []
for filepath in handoffs_dir.glob("*.md"):
parsed_date = parse_date_from_filename(filepath.name)
handoffs.append({
"path": str(filepath),
"filename": filepath.name,
"title": extract_title(filepath),
"status": check_completion_status(filepath),
"date": parsed_date,
"size": filepath.stat().st_size,
})
# Sort by date, most recent first
handoffs.sort(key=lambda x: x["date"] or datetime.min, reverse=True)
return handoffs
def format_date(dt: datetime | None) -> str:
"""Format datetime for display."""
if dt is None:
return "Unknown date"
return dt.strftime("%Y-%m-%d %H:%M")
def main():
# Get project path
project_path = sys.argv[1] if len(sys.argv) > 1 else os.getcwd()
handoffs = list_handoffs(project_path)
if not handoffs:
print(f"No handoffs found in {project_path}/.claude/handoffs/")
print("\nTo create a handoff, run: python create_handoff.py [task-slug]")
return
print(f"Found {len(handoffs)} handoff(s) in {project_path}/.claude/handoffs/\n")
print("-" * 80)
for h in handoffs:
print(f" Date: {format_date(h['date'])}")
print(f" Title: {h['title']}")
print(f" Status: {h['status']}")
print(f" File: {h['filename']}")
print("-" * 80)
print(f"\nTo resume from a handoff, read the document and follow the resume checklist.")
print(f"Most recent: {handoffs[0]['path']}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Validate a handoff document for completeness and quality.
Checks:
- No TODO placeholders remaining
- Required sections present and populated
- No potential secrets detected
- Referenced files exist
- Quality scoring
Usage:
python validate_handoff.py <handoff-file>
python validate_handoff.py .claude/handoffs/2024-01-15-143022-auth.md
"""
import os
import re
import sys
from pathlib import Path
# Secret detection patterns
SECRET_PATTERNS = [
(r'["\']?[a-zA-Z_]*api[_-]?key["\']?\s*[:=]\s*["\'][^"\']{10,}["\']', "API key"),
(r'["\']?[a-zA-Z_]*password["\']?\s*[:=]\s*["\'][^"\']+["\']', "Password"),
(r'["\']?[a-zA-Z_]*secret["\']?\s*[:=]\s*["\'][^"\']{10,}["\']', "Secret"),
(r'["\']?[a-zA-Z_]*token["\']?\s*[:=]\s*["\'][^"\']{20,}["\']', "Token"),
(r'["\']?[a-zA-Z_]*private[_-]?key["\']?\s*[:=]', "Private key"),
(r'-----BEGIN [A-Z]+ PRIVATE KEY-----', "PEM private key"),
(r'mongodb(\+srv)?://[^/\s]+:[^@\s]+@', "MongoDB connection string with password"),
(r'postgres://[^/\s]+:[^@\s]+@', "PostgreSQL connection string with password"),
(r'mysql://[^/\s]+:[^@\s]+@', "MySQL connection string with password"),
(r'Bearer\s+[a-zA-Z0-9_\-\.]+', "Bearer token"),
(r'ghp_[a-zA-Z0-9]{36}', "GitHub personal access token"),
(r'sk-[a-zA-Z0-9]{48}', "OpenAI API key"),
(r'xox[baprs]-[a-zA-Z0-9-]+', "Slack token"),
]
# Required sections for a complete handoff
REQUIRED_SECTIONS = [
"Current State Summary",
"Important Context",
"Immediate Next Steps",
]
# Recommended sections
RECOMMENDED_SECTIONS = [
"Architecture Overview",
"Critical Files",
"Files Modified",
"Decisions Made",
"Assumptions Made",
"Potential Gotchas",
]
def check_todos(content: str) -> tuple[bool, list[str]]:
"""Check for remaining TODO placeholders."""
todos = re.findall(r'\[TODO:[^\]]*\]', content)
return len(todos) == 0, todos
def check_required_sections(content: str) -> tuple[bool, list[str]]:
"""Check that required sections exist and have content."""
missing = []
for section in REQUIRED_SECTIONS:
# Look for section header
pattern = rf'(?:^|\n)##?\s*{re.escape(section)}'
match = re.search(pattern, content, re.IGNORECASE)
if not match:
missing.append(f"{section} (missing)")
else:
# Check if section has meaningful content (not just placeholder)
section_start = match.end()
next_section = re.search(r'\n##?\s+', content[section_start:])
section_end = section_start + next_section.start() if next_section else len(content)
section_content = content[section_start:section_end].strip()
# 50 chars minimum: roughly 1-2 sentences, enough to convey meaningful context
if len(section_content) < 50 or '[TODO' in section_content:
missing.append(f"{section} (incomplete)")
return len(missing) == 0, missing
def check_recommended_sections(content: str) -> list[str]:
"""Check which recommended sections are missing."""
missing = []
for section in RECOMMENDED_SECTIONS:
pattern = rf'(?:^|\n)##?\s*{re.escape(section)}'
if not re.search(pattern, content, re.IGNORECASE):
missing.append(section)
return missing
def scan_for_secrets(content: str) -> list[tuple[str, str]]:
"""Scan content for potential secrets."""
findings = []
for pattern, description in SECRET_PATTERNS:
matches = re.findall(pattern, content, re.IGNORECASE)
if matches:
findings.append((description, f"Found {len(matches)} potential match(es)"))
return findings
def check_file_references(content: str, base_path: str) -> tuple[list[str], list[str]]:
"""Check if referenced files exist."""
# Extract file paths from content (look for common patterns)
# Pattern 1: | path/to/file | in tables
# Pattern 2: `path/to/file` in code
# Pattern 3: path/to/file:123 with line numbers
patterns = [
r'\|\s*([a-zA-Z0-9_\-./]+\.[a-zA-Z]+)\s*\|', # Table cells
r'`([a-zA-Z0-9_\-./]+\.[a-zA-Z]+(?::\d+)?)`', # Inline code
r'(?:^|\s)([a-zA-Z0-9_\-./]+\.[a-zA-Z]+:\d+)', # With line numbers
]
found_files = set()
for pattern in patterns:
matches = re.findall(pattern, content)
for match in matches:
# Remove line numbers
filepath = match.split(':')[0]
# Skip obvious non-files
if filepath and not filepath.startswith('http') and '/' in filepath:
found_files.add(filepath)
existing = []
missing = []
for filepath in found_files:
full_path = Path(base_path) / filepath
if full_path.exists():
existing.append(filepath)
else:
missing.append(filepath)
return existing, missing
def calculate_quality_score(
todos_clear: bool,
required_complete: bool,
missing_required: list,
missing_recommended: list,
secrets_found: list,
files_missing: list
) -> tuple[int, str]:
"""Calculate overall quality score (0-100).
Scoring rationale:
- Start at 100, deduct for issues
- TODOs remaining (-30): Indicates incomplete work, major blocker
- Missing required sections (-10 each): Core context gaps
- Secrets detected (-20): Security risk, must be fixed
- Missing file refs (-5 each, max -20): Stale references
- Missing recommended (-2 each): Nice-to-have completeness
"""
score = 100
# Deductions with justifications
if not todos_clear:
# -30: TODOs indicate unfinished work; next agent will lack critical info
score -= 30
if not required_complete:
# -10 per section: Required sections are essential for handoff continuity
score -= 10 * len(missing_required)
if secrets_found:
# -20: Security risk; handoffs may be shared or stored in repos
score -= 20
if files_missing:
# -5 per file (max 4): Indicates stale refs; cap at -20 to avoid over-penalizing
score -= 5 * min(len(files_missing), 4)
# -2 per section: Recommended but not critical; minor impact on handoff quality
score -= 2 * len(missing_recommended)
score = max(0, score)
# Rating thresholds based on handoff usability:
# 90+: Comprehensive, ready to use immediately
# 70-89: Usable with minor gaps
# 50-69: Needs work before reliable handoff
# <50: Too incomplete to be useful
if score >= 90:
rating = "Excellent - Ready for handoff"
elif score >= 70:
rating = "Good - Minor improvements suggested"
elif score >= 50:
rating = "Fair - Needs attention before handoff"
else:
rating = "Poor - Significant work needed"
return score, rating
def validate_handoff(filepath: str) -> dict:
"""Run all validations on a handoff file."""
path = Path(filepath)
if not path.exists():
return {"error": f"File not found: {filepath}"}
content = path.read_text()
base_path = path.parent.parent.parent # Go up from .claude/handoffs/
# Run checks
todos_clear, remaining_todos = check_todos(content)
required_complete, missing_required = check_required_sections(content)
missing_recommended = check_recommended_sections(content)
secrets_found = scan_for_secrets(content)
existing_files, missing_files = check_file_references(content, str(base_path))
# Calculate score
score, rating = calculate_quality_score(
todos_clear, required_complete, missing_required,
missing_recommended, secrets_found, missing_files
)
return {
"filepath": str(path),
"score": score,
"rating": rating,
"todos_clear": todos_clear,
"remaining_todos": remaining_todos[:5], # Limit output
"todo_count": len(remaining_todos) if not todos_clear else 0,
"required_complete": required_complete,
"missing_required": missing_required,
"missing_recommended": missing_recommended,
"secrets_found": secrets_found,
"files_verified": len(existing_files),
"files_missing": missing_files[:5], # Limit output
}
def print_report(result: dict):
"""Print a formatted validation report."""
if "error" in result:
print(f"Error: {result['error']}")
return False
print(f"\n{'='*60}")
print(f"Handoff Validation Report")
print(f"{'='*60}")
print(f"File: {result['filepath']}")
print(f"\nQuality Score: {result['score']}/100 - {result['rating']}")
print(f"{'='*60}")
# TODOs
if result['todos_clear']:
print("\n[PASS] No TODO placeholders remaining")
else:
print(f"\n[FAIL] {result['todo_count']} TODO placeholders found:")
for todo in result['remaining_todos']:
print(f" - {todo[:50]}...")
# Required sections
if result['required_complete']:
print("\n[PASS] All required sections complete")
else:
print("\n[FAIL] Missing/incomplete required sections:")
for section in result['missing_required']:
print(f" - {section}")
# Secrets
if not result['secrets_found']:
print("\n[PASS] No potential secrets detected")
else:
print("\n[WARN] Potential secrets detected:")
for secret_type, detail in result['secrets_found']:
print(f" - {secret_type}: {detail}")
# File references
if result['files_missing']:
print(f"\n[WARN] {len(result['files_missing'])} referenced file(s) not found:")
for f in result['files_missing']:
print(f" - {f}")
else:
print(f"\n[INFO] {result['files_verified']} file reference(s) verified")
# Recommended sections
if result['missing_recommended']:
print(f"\n[INFO] Consider adding these sections:")
for section in result['missing_recommended']:
print(f" - {section}")
print(f"\n{'='*60}")
# Final verdict
if result['score'] >= 70 and not result['secrets_found']:
print("Verdict: READY for handoff")
return True
elif result['secrets_found']:
print("Verdict: BLOCKED - Remove secrets before handoff")
return False
else:
print("Verdict: NEEDS WORK - Complete required sections")
return False
def main():
if len(sys.argv) < 2:
print("Usage: python validate_handoff.py <handoff-file>")
print("Example: python validate_handoff.py .claude/handoffs/2024-01-15-auth.md")
sys.exit(1)
filepath = sys.argv[1]
result = validate_handoff(filepath)
success = print_report(result)
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()