handoff-mcp
README.md
# handoff-mcp
An MCP server that gives AI coding agents persistent memory across sessions.
When you close a Claude Code session and start a new one, the new session has no idea what the previous one was doing. handoff-mcp solves this by saving session context — tasks, decisions, blockers, and file pointers — to a local `.handoff/` directory that the next session can load automatically.
## The Problem
AI coding sessions are stateless. Every new session starts from zero:
- **"What was I working on?"** — the agent doesn't know
- **"What decisions were made?"** — lost with the previous context window
- **"What's left to do?"** — you have to re-explain everything
This gets painful fast on multi-session projects.
## How It Works
```
Session 1 Session 2
┌──────────────┐ ┌──────────────┐
│ Working... │ .handoff/ │ load_context │
│ │──────────────────>│ ↓ guidance │
│ save_context │ tasks/ │ save_context │
│ - close │ sessions/ │ (active) │
│ - summary │ config.toml │ ↓ work... │
│ - decisions │ │ save_context │
│ - blockers │ │ (close) │
└──────────────┘ └──────────────┘
```
At session start, the agent calls `handoff_load_context` to pick up where things left off. If no active session exists, the response includes `session_guidance` prompting the agent to establish one via `handoff_save_context` with `session_status: "active"` — this creates a persistent `.active.json` that survives interruptions. At session end, the agent calls `handoff_save_context` (defaulting to `session_status: "closed"`) to close the session.
## Installation
### Codex CLI plugin (recommended for Codex users)
Install the Handoff binary, add the GitHub marketplace, install the Codex Task
Loop plugin, and register the Handoff MCP server:
```bash
npm install -g handoff-mcp-server
codex plugin marketplace add alphaelements/handoff-mcp
codex plugin add handoff-task-loop-codex@handoff-mcp-marketplace
codex mcp add handoff -- handoff-mcp
```
Confirm the plugin and MCP server, then begin a new Codex session:
```bash
handoff-mcp --version
codex plugin list
codex mcp get handoff
```
Use `$handoff-session-loop` only when you want to explicitly run ready Handoff
tasks through the Codex developer, tester, and reviewer loop. For regular
session continuity, use the Handoff MCP tools and this repository's
[`AGENTS.md`](AGENTS.md) guidance.
**Local checkout:** replace `alphaelements/handoff-mcp` with the absolute path
to this repository in the marketplace command. Reinstall the plugin after an
update and start a new Codex session so it reloads the bundled skill.
**Direct MCP alternative:** users who do not need the task loop can install the
binary and run only `codex mcp add handoff -- handoff-mcp`. This supplies the
Handoff tools but not `$handoff-session-loop`.
For a controlled non-interactive loop, permit Handoff MCP writes for that one
invocation; otherwise `codex exec` can cancel task-state changes:
```bash
codex exec -c 'mcp_servers.handoff.default_tools_approval_mode = "auto"' \
--sandbox workspace-write '$handoff-session-loop <scoped task request>'
```
**Updating:** run `codex plugin marketplace upgrade handoff-mcp-marketplace`,
then reinstall with `codex plugin add handoff-task-loop-codex@handoff-mcp-marketplace`.
Update the binary separately with `npm install -g handoff-mcp-server@latest`
or `cargo install handoff-mcp --force`.
> **Claude Code compatibility:** Codex uses `$handoff-session-loop`; Claude
> Code uses `/session-loop` and `/research-loop`. The Codex plugin does not
> execute Claude commands, agents, or Workflow DSL.
### Claude Code Plugin (recommended)
The easiest way to install handoff-mcp is as a Claude Code plugin.
```bash
# 1. Install the binary (required — the plugin calls it)
npm install -g handoff-mcp-server
# or: cargo install handoff-mcp
# 2. Add the marketplace (GitHub repo)
/plugin marketplace add alphaelements/handoff-mcp
# 3. Install the plugin (MCP server + skills)
/plugin install handoff-mcp@handoff-mcp-marketplace
# 4. Apply the change
/reload-plugins
```
This registers the MCP server and all skills automatically — no manual
`.mcp.json` or skill file setup needed. The `handoff-mcp` plugin is enabled
on install, so no separate `/plugin enable` is needed. Run `/reload-plugins`
to pick up the change mid-session (a Claude Code restart also applies it).
> **Naming**: `handoff-mcp` is the *plugin* name; `handoff-mcp-marketplace`
> is the *marketplace* name (the `name` field in `.claude-plugin/marketplace.json`).
> Install commands always use `<plugin>@<marketplace>`.
#### Platform support
The npm package ships **prebuilt binaries** — no Rust toolchain, no compiler,
and no install scripts are needed. npm downloads only the binary matching your
platform.
| Platform | x64 | arm64 |
|---|---|---|
| Linux (glibc 2.35+) | ✅ | ✅ |
| macOS | ✅ | ✅ |
| Windows | ✅ | ✅ |
WSL works as a plain Linux install. For anything else — musl/Alpine, FreeBSD,
32-bit, or glibc older than 2.35 — install with `cargo install handoff-mcp`
instead, which builds from source.
If you already have a binary you built yourself, point the npm wrapper at it
with `HANDOFF_MCP_BINARY_PATH=/path/to/handoff-mcp`.
> **Note**: installing with `--omit=optional` skips the prebuilt binary and
> leaves the CLI unable to start. Reinstall without that flag.
>
> **Alpine / musl**: the Linux binaries are glibc-linked. npm may install one
> anyway, and it fails to exec with a confusing "not found" error even though
> the file is present. Use `cargo install handoff-mcp` on musl systems.
**Optional: task loop (automated TDD + research workflows)**
```bash
/plugin install handoff-task-loop@handoff-mcp-marketplace
/plugin enable handoff-task-loop@handoff-mcp-marketplace
/reload-plugins
```
Adds `/session-loop` (parallel TDD implementation, adversarial testing, Opus
review) and `/research-loop` (multi-agent investigation, verification, spec
drafting). See [plugin-task-loop/README.md](plugin-task-loop/README.md).
**Optional: memory auto-injection hooks**
```bash
/plugin install handoff-mcp-hooks@handoff-mcp-marketplace
/plugin enable handoff-mcp-hooks@handoff-mcp-marketplace
/reload-plugins
```
This adds hooks that inject relevant project memories on every prompt and file
edit. Disable anytime with `/plugin disable handoff-mcp-hooks@handoff-mcp-marketplace` —
the MCP server and skills remain active.
> **Important:** The hooks require a `handoff` MCP server entry in the
> project's `.mcp.json`. Run `handoff-mcp setup --mcp-json` in the project
> directory to add it automatically, or add it manually:
>
> ```json
> {
> "mcpServers": {
> "handoff": {
> "type": "stdio",
> "command": "handoff-mcp",
> "args": [],
> "env": {}
> }
> }
> }
> ```
>
> Without this entry, the hooks will show "not connected" errors on every
> prompt. The plugin's built-in MCP server is accessible to tools but not
> to hooks — this is a Claude Code limitation.
> The `handoff-task-loop` and `handoff-mcp-hooks` plugins ship with
> `defaultEnabled: false`, so they need an explicit `/plugin enable` step after
> install. The main `handoff-mcp` plugin is `defaultEnabled: true` and skips it.
**Installing the local development version instead**
If you are hacking on handoff-mcp and want Claude Code to load your local
checkout rather than the published GitHub version, register the repository root
(the directory containing `.claude-plugin/marketplace.json`) as a local
marketplace:
```bash
# 1. Build the binary and sync skills + plugin caches
./scripts/install-local.sh
# 2. Register the repo root as a local marketplace (first time only).
# Use the local path here — not the alphaelements/handoff-mcp shorthand.
/plugin marketplace add /absolute/path/to/handoff-mcp
# 3. Install and apply
/plugin install handoff-mcp@handoff-mcp-marketplace
/reload-plugins
```
After the first setup, re-run `./scripts/install-local.sh` whenever you change
the code, then restart Claude Code (or `/reload-plugins`) to load the rebuilt
version. Note that `install-local.sh` **only rebuilds the binary and refreshes
the plugin cache** — it does not register the marketplace or enable the plugin,
so steps 2 and 3 are a one-time bootstrap.
### Updating
If you installed handoff-mcp as a plugin, it updates along **two independent
paths**, and you need both. The plugin does not bundle the binary:
`plugin.json` registers the MCP server as `command: "handoff-mcp"`, which
Claude Code resolves on your `PATH`. So the marketplace ships the skills and
the plugin manifest, while npm or cargo ships the executable that actually
implements the MCP tools.
(Installed without the plugin, via `cargo install` or `npm install -g` alone?
Then only step 1 and the restart apply.)
Updating only the plugin leaves you on the old MCP tools. Updating only the
binary leaves you on the old skills.
```bash
# 1. Binary — this is what implements the MCP tools
npm install -g handoff-mcp-server@latest
# or: cargo install handoff-mcp --force
# 2. Marketplace catalog — fetch the new version list
/plugin marketplace update handoff-mcp-marketplace
# 3. Plugin — skills and manifest
/plugin update handoff-mcp@handoff-mcp-marketplace
# 4. Restart Claude Code
```
Update the optional plugins the same way if you installed them:
```bash
/plugin update handoff-task-loop@handoff-mcp-marketplace
/plugin update handoff-mcp-hooks@handoff-mcp-marketplace
```
Steps 1 and 3 update different things, and neither substitutes for the other.
`/plugin update` never touches the binary — it swaps the cached plugin directory,
which contains no executable at all — so no amount of restarting will give you
new MCP tools if you skipped step 1. Conversely, step 1 rewrites the file on disk
but the MCP server Claude Code already spawned keeps running the old image, so
you get the new tools only after step 4.
Step 4 is therefore not optional, and `/reload-plugins` is not a substitute: it
refreshes skills, not the MCP server process.
Do not expect step 1 to take effect on its own. An installer that overwrites a
*running* binary in place fails on Linux with `Text file busy`; installers that
replace the file instead (unlink, then create — what `install-local.sh` does)
succeed, and leave the already-running server executing the now-deleted old
image until it restarts. Either way, the new tools appear only after step 4.
Verify the update landed:
```bash
which handoff-mcp # the binary Claude Code will actually run
handoff-mcp --version
claude plugin list # plugin version, per marketplace
```
`claude plugin list` reports the version you actually have installed. Note that
`claude plugin details` reads the marketplace source instead, so it shows the
version on offer whether or not you have updated to it — don't use it to
confirm an update.
**Local development checkout**: `./scripts/install-local.sh` does both halves at
once (rebuilds the binary into `~/.local/bin` and refreshes the plugin cache).
Restart Claude Code afterwards.
**Troubleshooting**
- **Plugin or skills don't show up** — run `/reload-plugins`, or restart Claude
Code. As a last resort, `rm -rf ~/.claude/plugins/cache` and reinstall.
- **`plugin not found`** — refresh the catalog with
`/plugin marketplace update handoff-mcp-marketplace`, then reinstall.
- **MCP server won't start** — open `/plugin` → **Errors** tab, and confirm the
binary is on your `PATH` (`which handoff-mcp`).
- **You updated, but the MCP tools still behave like the old version** — you
almost certainly updated the plugin without updating the binary, or you
updated the binary but did not restart Claude Code. Work through the four
steps above in order.
- **An older `handoff-mcp` earlier on your `PATH` shadows the new one.** This is
the most common cause of "I updated and nothing changed", because
`npm install -g` and `cargo install` write to *different* directories. List
every copy and see which one wins:
```bash
type -a handoff-mcp # every match, in resolution order
for d in ${PATH//:/ }; do
[ -x "$d/handoff-mcp" ] && echo "$d -> $("$d/handoff-mcp" --version)"
done
```
The first line is the one Claude Code runs. Remove the stale copies (e.g.
`cargo uninstall handoff-mcp`, or delete the old file), or put the directory
holding the current binary earlier on your `PATH`.
- **`--version` says the right number but the behavior is old.** A version
string only changes when a release bumps it, so two builds of the *same*
version — a stale `cargo install` and a fresh one — report identically.
Compare the file itself rather than the version:
```bash
ls -l "$(which handoff-mcp)" # check the mtime
```
When in doubt, reinstall the binary and restart Claude Code.
- **Old versions pile up in the plugin cache.** Claude Code keeps each installed
version in its own directory under
`~/.claude/plugins/cache/handoff-mcp-marketplace/<plugin>/<version>/`. This is
harmless — the active version is recorded in
`~/.claude/plugins/installed_plugins.json` — but you can reclaim the space by
deleting the directories for versions you no longer use.
### cargo
```bash
cargo install handoff-mcp
```
### npm
```bash
npm install -g handoff-mcp-server
```
Both install the same binary, by different routes. `cargo install` fetches the
crate from [crates.io](https://crates.io/crates/handoff-mcp) and compiles it,
so it needs a Rust toolchain and works on any target Rust supports.
`npm install` downloads a prebuilt binary for your platform and compiles
nothing — see [Platform support](#platform-support) for the list.
### Build from source
```bash
git clone https://github.com/alphaelements/handoff-mcp.git
cd handoff-mcp
cargo build --release
```
## Setup (non-plugin)
If you installed via cargo/npm (without the plugin), register handoff-mcp as an
MCP server in Claude Code manually:
**Option A** — CLI (recommended):
```bash
claude mcp add -s user handoff -- handoff-mcp
```
The `-s user` flag registers it globally (available in all projects). Verify with `claude mcp get handoff`.
**Option B** — Manual edit of `~/.claude.json`:
```json
{
"mcpServers": {
"handoff": {
"type": "stdio",
"command": "handoff-mcp",
"args": []
}
}
}
```
### Enable automatic memory injection (optional)
If you installed via the plugin, use `handoff-mcp-hooks` instead (see above).
For non-plugin installs, run:
```bash
handoff-mcp setup
```
This installs Claude Code hooks into `~/.claude/settings.json` and adds a
`handoff` server entry to the project's `.mcp.json` (required for hooks to
connect). The command is interactive by default — use `-y` to skip prompts:
```bash
handoff-mcp setup -y # non-interactive (auto-approve everything)
handoff-mcp setup --mcp-json # only add .mcp.json entry (skip hooks)
handoff-mcp setup --check # check if hooks and .mcp.json are configured
```
Restart Claude Code after running setup.
You can check the current status or remove the hooks:
```bash
handoff-mcp setup --check # Show hook status
handoff-mcp setup --uninstall # Remove handoff hooks
```
The hooks fire on every prompt and file edit, which adds a small overhead per
interaction. If you want to stop automatic injection, run
`handoff-mcp setup --uninstall` — the memory tools themselves remain available
for manual use, only the automatic hooks are removed.
See [Automatic injection via hooks](#automatic-injection-via-hooks) for the
manual configuration alternative.
## Quick Start
1. **Initialize** a project:
The agent calls `handoff_init` with your project name. This creates a `.handoff/` directory:
```
.handoff/
├── config.toml # Project settings
├── sessions/ # Session history (TOML files)
└── tasks/ # Task tree (directories + TOML files)
```
2. **Load context** at session start — the agent calls `handoff_load_context`. If `session_guidance` is returned, the agent establishes an active session via `handoff_save_context` with `session_status: "active"` before starting work.
3. **Work normally** — create tasks, track progress, make decisions. The active session persists on disk, so progress survives interruptions.
4. **Save context** at session end — the agent calls `handoff_save_context` to close the active session with handoff data (summary, decisions, blockers, references).
> Add `.handoff/` to your `.gitignore` — it contains local working state, not code.
## Tools
### Core Session Management
| Tool | Purpose |
|------|---------|
| `handoff_init` | Initialize `.handoff/` directory for a project |
| `handoff_load_context` | Load session context, tasks, and git state at session start |
| `handoff_save_context` | Save session state — establish an active session or close it with handoff data |
| `handoff_update_session` | Incrementally update active session (toggle checklist, add decisions/notes/pointers) |
| `handoff_list_sessions` | List all sessions (open/active/paused/closed) with summary info |
| `handoff_get_session` | Get full detail of a specific session by ID |
| `handoff_fork_session` | Fork a new session from an existing one with context inheritance |
| `handoff_merge_sessions` | Merge multiple sessions into one with conflict detection |
### Task Management
| Tool | Purpose |
|------|---------|
| `handoff_list_tasks` | List tasks with filters (status, assignee, milestone, priority, label) |
| `handoff_get_task` | Get full task details (notes, done_criteria, schedule, etc.) |
| `handoff_update_task` | Create, update, or move tasks; supports `notes_append` for safe incremental notes and `requirement_ids` for linking to requirement SubItems |
| `handoff_check_criterion` | Toggle a single done_criteria item by index |
| `handoff_log_time` | Log hours worked — adds to `actual_hours`, deducts from `remaining_hours` |
| `handoff_bulk_update_tasks` | Update multiple tasks in one call (status, schedule, assignee, priority) |
### Metrics & Scheduling
| Tool | Purpose |
|------|---------|
| `handoff_get_metrics` | Project metrics: completion %, effort, overdue, budget, milestones |
| `handoff_get_capacity` | Work capacity for a date range, respecting calendar and assignee config |
| `handoff_auto_schedule` | Auto-schedule tasks based on dependencies, estimates, and capacity |
### Configuration & Team
| Tool | Purpose |
|------|---------|
| `handoff_get_config` | Read project configuration (full TOML as JSON) |
| `handoff_update_config` | Update config: settings, calendar, assignees, effort budget, gantt view |
| `handoff_list_assignees` | List team members with task counts and effort stats |
| `handoff_add_assignee` | Add a team member (`[assignees.<key>]`) |
| `handoff_update_assignee` | Update a team member's fields (partial; null clears a field) |
| `handoff_remove_assignee` | Remove a team member and unassign them from every task |
| `handoff_list_milestones` | List milestones (`[milestones.*]`) |
| `handoff_add_milestone` | Add a milestone (date, color, description) |
| `handoff_update_milestone` | Update a milestone (partial) |
| `handoff_remove_milestone` | Remove a milestone |
| `handoff_update_calendar` | Patch the project `[calendar]` (work hours, closed days, `day_hours`, schedule_mode) |
| `handoff_update_labels` | Set the project-level label vocabulary |
| `handoff_start_project` | Set `started_at` and optionally shift all task dates to the project start |
These CRUD tools and the VSCode extension write the same `config.toml`, so the
GUI and the MCP server stay in full parity. All writes are atomic (temp-file +
rename) so a concurrent reader never sees a partially-written file.
### Cross-Project
| Tool | Purpose |
|------|---------|
| `handoff_dashboard` | Overview of all handoff-enabled projects |
| `handoff_import_context` | Bulk import tasks and session data from documents |
| `handoff_refer` | Send a cross-project referral (bug, improvement, request) |
| `handoff_list_referrals` | List incoming referrals from other projects (summaries only) |
| `handoff_get_referral` | Fetch one incoming referral in full — details, suggested tasks, done_criteria, context |
| `handoff_update_referral` | Update referral status (open → acknowledged → resolved) |
### Timer Coordination
| Tool | Purpose |
|------|---------|
| `handoff_timer_start` | Start tracking time for a task — delegates to VSCode extension if alive, otherwise starts MCP fallback timer |
| `handoff_timer_stop` | Stop the timer and log elapsed hours to `actual_hours` — delegates to VSCode if alive |
| `handoff_timer_get_time` | Get current timer state (elapsed, authority, projected total) without stopping |
### Project Memory
| Tool | Purpose |
|------|---------|
| `handoff_memory_save` | Save a durable project memory (lesson/rule/convention/gotcha); detects exact and near-duplicate memories and hands near-duplicates back for AI-driven merge |
| `handoff_memory_query` | Return the memories most relevant to the current prompt/file (BM25 + scope-path boost); with a `session_id`, suppresses repeats already injected this session |
| `handoff_memory_delete` | Delete a memory by id (full id or unique prefix) |
| `handoff_memory_cleanup` | Manual/CLI housekeeping: silently merge exact duplicates, return near-duplicate/stale recommendations, gc old injection sidecars |
For usage best practices (granularity, scope_paths, conflict handling, cleanup), see `skills/handoff-memory/SKILL.md`.
See [Project Memory](#project-memory-1) below for what it is and how to wire automatic injection.
### Document Management
| Tool | Purpose |
|------|---------|
| `handoff_doc_save` | Create or update a document (auto-splits into sections) |
| `handoff_doc_get` | Read a document — full, meta, or single section |
| `handoff_doc_list` | List/search documents with BM25 and filters |
| `handoff_doc_delete` | Delete a document; unlinks from tasks |
| `handoff_doc_reassemble` | Reconstruct original Markdown from sections, with drift detection |
| `handoff_doc_update_section` | Replace a single section by seq (optimistic locking) |
| `handoff_doc_tree` | Walk family tree (ancestors/descendants/related) |
| `handoff_doc_graph` | Visualize inter-document relationships with optional verification status |
| `handoff_doc_trace` | Trace a document's lineage or dependency chain |
| `handoff_doc_query` | Context injection — staged full/outline, hook-driven |
| `handoff_doc_verify` | Verification matrix: generate, check, check_all, skip, sync, set_refs, add_item, set_dev_stage, set_priority, backfill_stable_ids |
| `handoff_doc_verify_status` | Verification progress summary with optional per-section details |
| `handoff_doc_req_status` | Requirements progress across documents (by status / priority / category, coverage, per-task coverage) |
| `handoff_doc_req_list` | List individual requirements with filters (priority, dev_stage, category, tests, task), sorting and paging |
| `handoff_doc_req_import` | Turn a Markdown requirement tree into requirement items with stable IDs and priorities (dry-run first) |
| `handoff_doc_req_scan` | Suggest code/test references for requirements by scanning source files for requirement IDs |
| `handoff_doc_req_impact` | Find requirements affected by changed files (or the current git diff) |
| `handoff_doc_analyze` | Read-only heuristic scan (import step 1) |
| `handoff_doc_import` | Atomic bulk write after analysis (import step 3) |
| `handoff_trace_record` | Record one execution batch (pass/fail/blocked/…) against layer-document items, as a new file under `.handoff/runs/` |
| `handoff_trace_report` | V-model coverage/gap report across every layer document, task links, and recorded runs; also refreshes `.handoff/docs/_trace_report.json` for editor integrations |
| `handoff_trace_slice` | Neighborhood view around one task or item (up/down/both the requirement↔verification graph), for progressive-disclosure traversal |
| `handoff_trace_history` | Every recorded execution result for one item, newest first |
| `handoff_trace_baseline` | Snapshot the current trace graph's coverage, list/diff past baselines |
| `handoff_trace_delta` | Change-proposal (delta) lifecycle: create/list/apply/reject a pending bundle of ops for human review |
| `handoff_trace_ingest` | Parse `cargo test --format json` JSONL output and record each matched test's result via `trace_record` |
| `handoff_trace_scaffold` | Generate one verification-layer item per acceptance-criteria bullet of a source item |
| `handoff_trace_suspect` | List/clear/baseline the 3 suspect kinds a link/task/result falls into when its upstream definition changed |
| `handoff_trace_impact` | Read-only impact analysis for a proposed change — never writes anything |
| `handoff_trace_lint` | Read-only lint over the whole trace graph (structure, tailoring, drift, format rules) |
| `handoff_trace_matrix` | Read-only coverage/gap matrix for one or more layer documents |
| `handoff_trace_next` | Read-only prioritized worklist of V-model items/tasks still needing attention |
| `handoff_trace_test_run` | Test run definitions and dynamically-computed progress (create/record/progress/list) |
| `handoff_trace_propose` | Read-only suggestion of existing V-model items covering a task, plus a ready-to-review template for a new one |
| `handoff_trace_tasks` | Generate one task per V-model item still missing the task role it needs |
| `handoff_trace_update` | Bulk-mutation entry point over several op kinds (upsert_item, link, unlink, suspect clear, approval, …) in one call |
Documents live in `.handoff/docs/` as single `_doc.<slug>.md` files (YAML
frontmatter + body). Large Markdown is split into sections on save;
`handoff_doc_reassemble` reconstructs the original with drift detection, and
`handoff_doc_query` feeds staged (outline-first, then full-text) context to
the agent — the same mechanism that powers the hook-driven injection described
below. `handoff_doc_verify` provides a verification matrix for tracking
per-section review status, implementation/test references, and staleness
detection after spec changes.
**Requirements traceability.** Requirement items (sub-items of the verification
matrix) carry a stable ID (e.g. `C01-FR-101`), priority (P0–P3), development
stage (`not_started` → `in_progress` → `implemented` → `tested` → `verified`)
and code/test references. Link a task to requirements with
`handoff_update_task(task={id, requirement_ids: [...]})`; the link is recorded on
both sides (with an `implements`/`executes` role), and the development stage
follows the linked `implements` tasks' status automatically. Progress is
summarized in `.handoff/docs/_requirements_summary.json`, which the VS Code
extension reads.
**V-model layer documents.** Pass `layer` to `handoff_doc_save` (one of
`requirement`/`basic_spec`/`detailed_spec`/`acceptance`/`system_test`/
`unit_test`) to turn a document's Markdown body into the source of truth for
its requirement/verification items — every save re-parses ID-prefixed
headings (`REQ-001`, `SPEC-012`, `AT-001`, …) with `refines`/`verifies`/
`priority`/`method`/`test` attributes into the verification matrix. Record
execution results with `handoff_trace_record`, then get a full
coverage/gap report (`handoff_trace_report`) or a neighborhood slice around
one task/item (`handoff_trace_slice`). See the `handoff-docs` skill's
"V-model Layer Documents" section for templates and the layer-skip
(inline-verification) workflow.
## V-model Traceability (optional)
handoff-mcp can track requirements/design/test coverage as a V-model: six
built-in layers (`req`/`spec`/`design`/`acceptance`/`system_test`/`unit_test`),
linked by `refines`/`verifies`, with lint, suspect/reverify tracking, and
baseline diffs. This is entirely separate from the simpler `doc_req_import`
freeform-SubItem workflow described above — **the two are mutually exclusive
on a single document** (see `handoff-trace` skill §15 if you need to migrate
one to the other).
To get started:
1. Add a `[trace]` section to `.handoff/config.toml`:
```toml
[trace]
profile = "standard"
```
2. Save a document with `handoff_doc_save(layer="req", trace_profile="standard", body="...")`
— the body's headings (e.g. `### REQ-001 <title>`) become SubItems automatically.
3. Use `handoff_trace_lint` to check structural issues, `handoff_trace_matrix`
for a coverage table, `handoff_trace_next` for a prioritized worklist, and
`handoff_trace_report`/`handoff_trace_slice` for derivation graphs.
Full guide: **`skills/handoff-trace/SKILL.md`** — every layer, profile, body
notation rule, lint rule, and the `trace_*` tool family (17 tools) live there.
### Task Data Model
Tasks are stored as a directory tree with status encoded in filenames:
```
tasks/
├── t1-implement-auth/
│ ├── _task.done.json
│ ├── t1.1-design-schema/
│ │ └── _task.done.json
│ └── t1.2-write-handlers/
│ └── _task.in_progress.json
└── t2-deploy-staging/
└── _task.blocked.json
```
Statuses: `todo` | `in_progress` | `review` | `done` | `blocked` | `skipped`
Each task can have:
- **Assignee** — team member key (matches `[assignees.<key>]` in config.toml)
- **Priority** — `low` / `medium` / `high`
- **Labels** — free-form tags
- **Done criteria** — checklist items (all must be checked before `done` transition)
- **Links** — URLs to issues, MRs, or docs
- **Notes** — markdown description
- **Schedule** — `start_date`, `due_date`, `estimate_hours`, `actual_hours`, `remaining_hours`, `milestone`, `pinned`
- **Dependencies** — task IDs this task depends on (circular deps rejected)
### Session Context
When saving context, the agent can record:
- **Summary** — one-line description of what happened
- **Decisions** — what was decided and why, with confidence levels (`confirmed` / `estimated` / `unverified`)
- **Blockers** — what's preventing progress
- **Checklist** — items for the next session
- **Handoff notes** — categorized as `caution`, `context`, or `suggestion`
- **References** — links to files, issues, MRs, wiki pages, or URLs
- **Context pointers** — specific files and line ranges the next session should look at
- **Git state** — current branch, recent commits, and dirty files (captured automatically)
### Dashboard
`handoff_dashboard` scans directories for projects with `.handoff/` and shows a summary:
```
## my-project (3 tasks)
- [in_progress] Implement auth (high)
- [todo] Add tests (medium)
- [blocked] Deploy staging (medium)
## other-project (1 task)
- [review] Update README (low)
```
## Configuration
`.handoff/config.toml`:
```toml
[project]
name = "my-project"
description = "Project description"
[settings]
history_limit = 20 # Max closed sessions to keep
done_task_limit = 10 # Max completed tasks to show
auto_git_summary = true # Capture git state automatically
require_estimate_hours = true # Require estimate_hours on leaf tasks (default true)
multi_session = true # Allow multiple active sessions (default true for new projects)
ai_estimate_multiplier = 0.2 # Multiplier turning human estimates into AI-effort hours
timer_provider = "auto" # "auto" | "vscode" | "mcp" | "off"
timer_authority_ttl_secs = 30 # Heartbeat freshness TTL for authority.json
timer_idle_timeout_minutes = 10 # Idle pause threshold for MCP fallback timer
[dashboard]
scan_dirs = ["~/pro/"] # Directories to scan for dashboard
[calendar]
work_hours_per_day = 8
closed_weekdays = ["sat", "sun"]
closed_dates = ["2026-12-25"]
open_dates = []
schedule_mode = "auto" # "auto" or "manual"
overwork_limit_percent = 150
[calendar.day_hours]
fri = 4 # Per-weekday hour overrides
[effort_budget]
total_hours = 500 # Total project effort cap
[assignees.alice]
display_name = "Alice Chen"
color = "#4A90D9"
work_hours_per_day = 8
closed_weekdays = [1, 2] # Per-assignee overrides
[assignees.bob]
display_name = "Bob Martinez"
color = "#E74C3C"
work_hours_per_day = 6
[gantt_view]
sort = "start" # start, id, id-desc, status
zoom = "week" # day, week, month
mode = "compare" # plan, actual, compare
[trace]
profile = "standard"
# See `handoff-trace` skill for layers/done_guard/id_prefixes/custom profiles.
```
All configuration sections can be updated via `handoff_update_config` with dot-notation keys (e.g., `"calendar.work_hours_per_day": 7`).
### Estimates and AI effort
handoff-mcp distinguishes the **raw human-effort estimate** you record on a task
from the **AI-effort hours** used in scheduling and metrics:
- **`require_estimate_hours`** (default `true`) — `handoff_update_task` rejects
creating or updating a *leaf* task in `in_progress` / `review` / `done` without
`schedule.estimate_hours > 0`. Parent tasks (with children) and tasks in `todo` /
`blocked` / `skipped` are exempt, and an estimate already on the task satisfies
the requirement. Set to `false` to opt out.
- **`ai_estimate_multiplier`** (default `0.2`) — the factor applied to raw
estimates to model how long the work takes when an AI agent does it. Always
record the *raw human-effort* estimate; the multiplier is applied at
aggregation time by `handoff_get_metrics` (`total_adjusted_estimate_hours` and
per-milestone `adjusted_estimate_hours`) and `handoff_get_capacity`. Raw values
are never overwritten.
## Project Memory
Sessions answer *"what was I doing last time?"*. **Memory** answers a longer-lived
question: *"what should every session in this project always know?"* — durable
lessons, rules, conventions, and gotchas that outlive any one session.
> **Note:** The memory tools (`handoff_memory_save`, `handoff_memory_query`, etc.) can always be
> called directly by the agent. For **automatic** injection — where relevant
> memories are surfaced on every prompt without the agent asking — you need to
> configure Claude Code hooks. See [Automatic injection via hooks](#automatic-injection-via-hooks).
Memories live in `.handoff/memory/` (one JSON file per memory, plus per-session
`injected/` sidecars). A built-in multilingual similarity engine (Japanese /
English, dictionary-free) ranks relevance and detects duplicates, all in-memory
and sub-millisecond.
### Using it directly
The agent can call the memory tools at any time:
- `handoff_memory_save` — record a memory. An exact duplicate is reported (not
rewritten); a near-duplicate comes back as a `conflict` with both bodies so the
agent can merge them (`merge_into=<id>`, `absorb_ids=[…]`) or save separately
with `force=true`. **handoff-mcp never merges for you** — it surfaces both
bodies and lets the agent decide.
- `handoff_memory_query` — fetch the memories most relevant to some text and/or files.
- `handoff_memory_delete` / `handoff_memory_cleanup` — prune and de-duplicate the store.
### Automatic injection via hooks
MCP is request/response — the server cannot push a memory into the agent's
context on its own. **Claude Code hooks** close that gap: they fire regardless of
what the agent intends, call `handoff_memory_query`, and inject the matching memories as
`additionalContext`. A per-session diff (keyed on the hook `session_id`) ensures
the same memory is **not injected twice in one session** — and an *edited* memory
(new content hash) is re-injected.
The same hooks also call `handoff_doc_query`, so relevant documents (saved via
`handoff_doc_save`) are staged into context alongside memories — outline first,
then full text as relevance/budget allows.
| Event | Calls | Effect |
|-------|-------|--------|
| `UserPromptSubmit` | `handoff_memory_query` (prompt text), `handoff_doc_query` (prompt text) | Inject memories and documents relevant to the prompt |
| `PreToolUse` (`Edit\|Write\|MultiEdit`) | `handoff_memory_query` (file path), `handoff_doc_query` (file path) | Inject memories and documents scoped to the file being edited |
`handoff_memory_cleanup` (merge exact duplicates, gc old sidecars) is not wired
to a hook — call it manually or from a CLI/cron job when you want housekeeping.
> The bundled `plugin-hooks/hooks/hooks.json` (used by the "Handoff MCP — Memory
> & Document Hooks" plugin) already wires both `handoff_memory_query` and
> `handoff_doc_query` on `UserPromptSubmit` and `PreToolUse`; the JSON examples
> below show the equivalent hand-written config for non-plugin setups.
> **Wire hooks in your *user/global* settings, not in the repo.** Hooks are a
> personal workflow choice; the handoff-mcp repo does not ship a `.claude/`
> hooks config, and you should not commit one into a shared project. Put the
> config in `~/.claude/settings.json` (global) or your own
> `.claude/settings.local.json` (git-ignored).
**Native `mcp_tool` hook (preferred).** Recent Claude Code versions can call an
MCP tool from a hook directly, with no wrapper script. In
`~/.claude/settings.json`:
```json
{
"hooks": {
"UserPromptSubmit": [
{ "hooks": [ {
"type": "mcp_tool", "server": "handoff", "tool": "handoff_memory_query",
"input": { "project_dir": "${cwd}",
"session_id": "${session_id}", "text": "${prompt}" }
} ] }
],
"PreToolUse": [
{ "matcher": "Edit|Write|MultiEdit", "hooks": [ {
"type": "mcp_tool", "server": "handoff", "tool": "handoff_memory_query",
"input": { "project_dir": "${cwd}",
"session_id": "${session_id}", "tool_name": "${tool_name}",
"text": "${tool_input.file_path}",
"file_paths": ["${tool_input.file_path}"] }
} ] }
]
}
}
```
(`server` must match the name you registered handoff-mcp under — `handoff` in the
[Setup](#setup) examples.)
**Wrapper script fallback.** If your Claude Code version doesn't support the
`mcp_tool` hook type, use the bundled `command` wrapper
[`scripts/handoff-memory-hook.py`](scripts/handoff-memory-hook.py). It reads the
hook JSON on stdin, calls the server over JSON-RPC, and emits
`additionalContext` — the memory tools return their payload as a JSON *string* so
both paths parse it identically. Point all three hooks at it:
```json
{
"hooks": {
"UserPromptSubmit": [
{ "hooks": [ { "type": "command",
"command": "/path/to/handoff-mcp/scripts/handoff-memory-hook.py" } ] }
],
"PreToolUse": [
{ "matcher": "Edit|Write|MultiEdit", "hooks": [ { "type": "command",
"command": "/path/to/handoff-mcp/scripts/handoff-memory-hook.py" } ] }
]
}
}
```
The script resolves the `handoff-mcp` binary from `PATH` (override with
`HANDOFF_MCP_BIN`) and **fails safe**: on any error it prints nothing and exits
0, so a memory miss is silent and never blocks your prompt.
### Upgrading from a version with a `SessionStart` cleanup hook
Versions before this fix had `handoff-mcp setup` (and the
`handoff-mcp-hooks` plugin) install a **synchronous `SessionStart` hook**
that ran `handoff_memory_cleanup` on every session start. Under many
parallel sub-agents (e.g. `/research-loop`), that hook could pile up heavy
cleanup calls on the single-threaded server and hang your editor.
The `SessionStart` cleanup hook has been removed entirely — `memory_cleanup`
is still available, but only via manual/CLI invocation, never auto-fired.
If you already ran `handoff-mcp setup` before this change, migrate with
**one** of the following:
- **Re-run setup** (recommended): `handoff-mcp setup`. It now detects and
automatically strips the legacy `SessionStart` cleanup hook while leaving
your other handoff hooks untouched. Use `handoff-mcp setup --check` first
if you want to confirm whether the legacy hook is present before touching
anything.
- **Manual edit**: open `~/.claude/settings.json` and delete the
`hooks.SessionStart` entry whose `tool` is `handoff_memory_cleanup` (remove
the whole `SessionStart` key if that was its only entry).
- **Plugin users**: `/plugin update handoff-mcp-hooks@handoff-mcp-marketplace`
to pick up the new `hooks.json`, then restart Claude Code. This one ships
inside the plugin, so no binary update is needed.
Restart Claude Code after any of the above for the change to take effect.
### Optional workflow observer (Claude Code)
For workflow performance investigations, `scripts/claude-workflow-observer.js`
normalizes Claude Code lifecycle hooks into an append-only JSONL file. It is
**not enabled by this repository or by plugin frontmatter**: copy the command
paths from [`plugin-hooks/observer/hooks.json`](plugin-hooks/observer/hooks.json)
into your own `~/.claude/settings.json` or managed settings, replacing both
`/ABSOLUTE/PATH/TO/...` placeholders. Keep the log outside the repository, for
example `~/.local/state/handoff-mcp/claude-workflow.jsonl`.
The observer records `agent_started`, `phase_changed`, `tool_started`,
`tool_finished`, and `agent_finished`. It uses `session_id` as the fallback
agent/run correlation key and preserves a supplied `agent_id`, parent session,
task, phase, and `tool_use_id` where the hook runtime supplies them. It does
not record prompt text, tool input, command arguments, or command output; Bash
events retain only the executable name to support duration aggregates.
Set these environment variables in the hook command only when the workflow
wrapper can supply stable values:
| Variable | Purpose |
|-------|---------|
| `HANDOFF_OBSERVER_LOG` | Absolute JSONL destination; if omitted, no event is written. |
| `HANDOFF_WORKFLOW_RUN_ID` | Parent workflow-run correlation ID. |
| `HANDOFF_WORKFLOW_TASK_ID` | Handoff task identifier. |
| `HANDOFF_WORKFLOW_PHASE` | Workflow phase (for example `implement`). |
The hook always exits successfully, including a malformed event or unwritable
log, and it does not print hook output. This makes it observational only; it
cannot enforce command policy. Aggregate a completed or still-running run with:
```bash
node /ABSOLUTE/PATH/TO/claude-workflow-observer.js summarize \
~/.local/state/handoff-mcp/claude-workflow.jsonl
```
The summary reports `status: "running"` until an explicit `SessionEnd` or
`SubagentStop` arrives, even if a native journal contains only a started/result
pair. It also reports turn/tool counts, tool wait, estimated model wait,
per-executable duration, and the longest command. Estimated model wait is wall
time not covered by paired hook-visible tools, so it includes streaming and any
other uninstrumented interval; it is not a token-level OTel replacement.
Claude Code's OpenTelemetry export remains complementary: it can provide
process-level traces, but the command hook is the source for this normalized
per-agent lifecycle stream. The hook input varies by Claude Code version:
`session_id` and `tool_use_id` are usable fallback correlation keys, while an
independent workflow run, task, and phase must be supplied by the wrapper.
### Memory settings
All under `[settings]` in `.handoff/config.toml`, all with safe defaults
(existing projects need no change), all settable via `handoff_update_config`:
| Key | Default | Meaning |
|-----|---------|---------|
| `memory_enabled` | `true` | Master switch. When `false`, all four memory tools return a benign empty result and write nothing |
| `memory_dup_threshold` | `0.72` | Jaccard similarity at/above which a save is a near-duplicate conflict and cleanup groups a cluster |
| `memory_query_min_score` | `0.5` | BM25 relevance floor for `handoff_memory_query` results |
| `memory_query_limit` | `5` | Max memories returned per query |
| `memory_stale_days` | `60` | Days without a reference before a memory is flagged stale |
| `memory_injected_gc_days` | `14` | Age at which per-session injection sidecars are garbage-collected |
## CLI API
Since v0.15.0, every MCP tool is also callable directly from the shell:
```bash
handoff-mcp <group> <action> [--key value ...]
```
All output is JSON on stdout, suitable for scripting and programmatic use
(e.g. `child_process.execFile` from a VSCode extension).
**Examples:**
```bash
# Memory operations
handoff-mcp memory save --text "Always use atomic_write" --kind lesson --tags safety,io
handoff-mcp memory query --text "atomic" --limit 5
handoff-mcp memory delete --id m-20260630-...
# Task management
handoff-mcp task list --status-filter todo
handoff-mcp task update --id t1 --title "New task" --status todo --estimate-hours 2
handoff-mcp task log-time --task-id t1 --hours 0.5
# Session and metrics
handoff-mcp session load
handoff-mcp metrics
handoff-mcp dashboard
```
**Available groups:** `init`, `task`, `session`, `config`, `memory`,
`referral`, `assignee`, `milestone`, `calendar`, `labels`, `project`,
`metrics`, `capacity`, `schedule`, `dashboard`, `timer`.
Run `handoff-mcp --help` to see all groups, or `handoff-mcp <group> --help`
for actions within a group. See the
[CLI API Reference](https://github.com/alphaelements/handoff-mcp/wiki/CLI-API-Reference)
on the wiki for the full command list.
## MCP Resources
| URI | Description |
|-----|-------------|
| `handoff://sessions` | Active session data (JSON) |
| `handoff://config` | Project configuration (TOML) |
## Recommended CLAUDE.md Setup
Copy the template from [`templates/claude-md-section.md`](templates/claude-md-section.md) into your project's `CLAUDE.md`. It declares that the project uses handoff-mcp and points to the `handoff` skill for the full procedure. Plugin users get the skill automatically; non-plugin users should copy `skills/` to `~/.claude/skills/`.
## Skills
This repository includes skill files that make handoff behavior automatic in Claude Code:
| Skill | Purpose |
|-------|---------|
| `handoff` | Core session lifecycle, task management, metrics, scheduling |
| `handoff-load` | Quick session-start procedure |
| `handoff-docs` | Document management — save, search, verify, import, family tree |
| `handoff-trace` | V-model traceability — layers, profiles, lint, suspect/reverify, baselines |
| `handoff-memory` | Memory CRUD, conflict handling, cleanup |
| `handoff-refer` | Cross-project referrals |
| `handoff-import` | Bulk import from documents |
**Plugin users**: all skills are included automatically.
**Manual setup**: copy the skills to your user skills directory:
```bash
cp -r skills/* ~/.claude/skills/
```
## Compatibility
- **Claude Code** — fully supported (stdio transport)
- **Other MCP clients** — any client supporting the MCP stdio transport
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md) for local development gates, including
the performance budget harness for changes to the storage/handler layers.
## License
MIT
This server cannot be deployed
Maintenance
ActivityActive
ResponsivenessNo issues