Skip to content

session-flow/handoff + find-handoff: resume-prompt path is rootless, so it resolves against a real-but-wrong directory when cwd is not the worked-in repo root #1644

Description

@kyle-sexton

Summary

/session-flow:handoff writes the save-point file to an absolute location but emits a rootless
repo-relative path
in the copy-paste resume prompt. When the resuming session's cwd is not the root
of the repository the handoff was written in, that path resolves against the wrong root. If a
.work/handoffs/ directory happens to exist under that wrong root — which it does on any machine
that has run /handoff from the home directory before — the failure presents as "the file is
missing"
rather than "the path has no root", which is the most expensive shape to diagnose.

The same single-root assumption sits in /session-flow:find-handoff, the skill that exists
specifically to recover this situation, so the designated recovery path does not recover it either.

Observed occurrence

Session e67f9589-6925-4fc0-901e-41e4c0abec05, session-flow 0.17.14.

The session's cwd was C:\Users\<user> (not a git repository); all of its work was in a separate
local-only repository, D:\repos\...\claude-code-proxy. The transcript records both facts: every
cwd field in it holds either C:\Users\<user> or the session's scratchpad temp directory — the
worked-in repository never appears as a cwd.

The write went to the worked-in repo, which is reasonable (see "The write location is not the
defect"):

D:\repos\...\claude-code-proxy\.work\handoffs\20260726T234832Z-handoff-devils-advocate-applied.md

The emitted resume prompt, verbatim from the transcript's assistant text event:

`/clear`, then copy everything between the dashed lines:

──────────────────────────────────────────────────────────
Read @.work/handoffs/20260726T234832Z-handoff-devils-advocate-applied.md and continue its remaining next steps.
Prior session: e67f9589-6925-4fc0-901e-41e4c0abec05.
──────────────────────────────────────────────────────────

The fresh session started in the same cwd, C:\Users\<user>. That directory holds its own
.work/handoffs/ with 39 files matching *-handoff-*.md from unrelated sessions — so
@.work/handoffs/<file> resolved into a real, populated, wrong directory. The recovering session
reported four tool calls to recover the path (glob the wrong directory, search the home tree, grep
the prior transcript for the filename, grep it again for "file_path" to extract the absolute path);
that count is the recovering session's own report, not a figure this issue measured.

Note that the absolute root was available to the producer at write time and it did record it — inside
the handoff body: "Written against feat/logging-proxy at 5371e2d in
D:\repos\...\claude-code-proxy"
. Only the resume prompt, the one artifact the operator actually
carries across /clear, lost it.

The relative path is contract-specified, not a model slip

reference/save-point.md, "Emit the copy/paste resume prompt", full-path shape:

Read @<handoffs-dir>/<TS>-handoff-<topic>.md and continue its remaining next steps.
Prior session: <UUID>.

and immediately after it:

<handoffs-dir> is the path the write step actually used — the resolved
<memory_dir>/handoffs/ (default .work/handoffs/). Never emit a
default the file was not written to.

<memory_dir> is a repo-relative value by contract (memory_dir: .work in
.claude/topic-docs.yaml; default .work), so "the path the write step actually used" renders as a
rootless relative path. The guard that follows it is about the wrong axis: it prevents emitting a
memory_dir segment the write did not use, and says nothing about the root that segment hangs off.

skills/find-handoff/SKILL.md confirms this is the expected producer contract rather than a
one-off deviation, twice — in the detection ladder:

Apply the same placeholder filter as file mode … Confirm the referenced file exists on disk —
and resolve a relative directive path against the source transcript's cwd field, not the
current session's cwd
: the producer emits repo-relative paths (e.g.
Read @.work/handoffs/…) …

and in Gotchas:

Relative directive paths resolve against the SOURCE transcript's cwd. The producer emits
repo-relative paths; a cross-repo recovery that checks existence from the current session's cwd
falsely reports the file missing.

So the model did what the engine specifies. The defect is that the specified output shape carries no
root anchor and nothing in the engine requires the emitted path to be resolvable from the resuming
session's cwd.

This is not only a cross-repo problem. @.work/handoffs/… also fails when the resuming session's
cwd is a subdirectory of the correct repository. Cross-repo is the worst presentation (a real-but-wrong
root that mimics a missing file), not the only trigger — a fix that special-cases cross-repo would
leave the subdirectory case broken.

Second surface: find-handoff has the same single-root assumption

Both rungs of the recovery ladder fail on this input.

Rung 1 — known-location glob. The skill's precompute is cwd-relative:

Default-location handoffs (this repo): !`ls -1t .work/handoffs/*-handoff-*.md 2>/dev/null | head -5 …`

and the ladder step filters candidates only on frontmatter and mtime:

Glob *-handoff-*.md, keep only files whose frontmatter is type: handoff, rank by mtime. A
strong, recent candidate → jump to step 4 …

Run from C:\Users\<user>, that glob returns 39 conforming type: handoff files from unrelated
repositories and the target is not among them. There is no repo-correlation check anywhere in the
rung — nothing in the text would reject a same-cwd, different-repo candidate. (Whether the model
would have short-circuited on one turns on its own read of "strong, recent"; the missing check is the
finding, not a predicted behavior.)

Rungs 2-3 — transcript scan. This rung does find the correct directive, and then discards it.
The rule quoted above resolves the relative path against the source transcript's cwd — which for
this session is C:\Users\<user> or the scratchpad temp directory, neither of which contains the
file — and the rung then requires "Confirm the referenced file exists on disk." The correct candidate
fails that existence check and is dropped. The rule written specifically to make cross-repo recovery
work is the rule that breaks recovery here, because it assumes the producer's cwd is the repo the
file was written into.

Anchoring the producer's emitted path fixes both surfaces at once: an absolute path needs no cwd
resolution, so rung 2-3's existence check succeeds without the cwd inference, and rung 1 stops
being the only path that can find a cross-root handoff.

The write location is not the defect

The write went to the repository the session was working in, which is the useful outcome — the
save-point lives with the work. Worth recording that it is nonetheless off-ladder: per the
topic-docs contract's "Resolution order", cwd C:\Users\<user> has no project root, and that branch
says interactive → ask (current directory or an explicit path); non-interactive →
${CLAUDE_PLUGIN_DATA}/topic-docs/handoffs/. The session was interactive and did neither; it
inferred the worked-in repo instead.

The gap is that the resolution order has no rung for "the session is working in a repository that is
not cwd's project root," so a correct-in-spirit write lands outside the ladder — and the render step
then has no root to render against. Deciding whether to add that rung is a separate call from
anchoring the path, and the path fix does not depend on it.

Proposed fix direction, and the tradeoff

The obvious candidate is to emit an absolute path in the resume prompt (keeping <memory_dir> as
the configured relative value for resolution, and rendering the resolved absolute path into the
directive). There is already precedent for absolute inside this engine's own contract:
reference/topic-docs.md specifies that the no-project-root fallback "lands handoffs under
${CLAUDE_PLUGIN_DATA}/topic-docs/handoffs/ with the absolute path announced prominently" — absolute
is already the answer on the one branch where relative has no anchor. This is the same condition,
reached a different way.

Two objections a reviewer will reasonably raise, and where they land:

  • "No hardcoded/machine-local absolute paths." The topic-docs redaction bar ("committed evidence
    is distilled — no machine-local absolute paths") and CLAUDE.md's repo-agnostic rule both govern
    committed artifacts and plugin content. A resume prompt is neither: it is runtime output the
    operator pastes on the same machine, and the handoff file itself is gitignored memory tier. The
    handoff body already carries the absolute repo path today.
  • Multi-machine / multi-checkout portability. This is the real tradeoff. An absolute path breaks
    if the handoff is resumed on a different machine, or in a different checkout of the same repo — and
    handoff's own "When to invoke" lists "Sharing state with another session or machine". A relative
    path is portable if the resuming session's cwd is the repo root, which is exactly the condition
    that failed here.

Shapes worth weighing rather than one asserted answer:

  1. Absolute path in the directive (simplest; loses cross-machine portability).
  2. Both — absolute directive plus the repo-relative path and repository identity as a stated line
    below it, so a different-machine resume has what it needs to re-resolve.
  3. Keep relative but make the root explicit and machine-independent (e.g. name the repository /
    remote / checkout the path is relative to, so a mismatch surfaces as a wrong root rather than a
    missing file).

Whichever shape is chosen, find-handoff's detection contract moves with it — save-point.md
already states that changing the prompt format is a knowing break of that contract — and rung 1's
missing repo-correlation check should be closed in the same change.

Reproduction

  1. Start a session whose cwd is a directory that is not the repository the work happens in, and
    which itself contains a .work/handoffs/ directory (a home directory that has run /handoff
    before is the common case).
  2. Do work in a different repository and run /session-flow:handoff.
  3. Observe: the file is written under <worked-in-repo>/.work/handoffs/, and the rails prompt reads
    Read @.work/handoffs/<TS>-handoff-<topic>.md.
  4. /clear and paste. The @-reference resolves under cwd, into the unrelated .work/handoffs/, and
    the file is reported missing.
  5. Run /session-flow:find-handoff. Rung 1's glob returns the unrelated handoffs; rungs 2-3 locate
    the correct directive and then discard it on the existence check.

Version verified

session-flow 0.17.14. reference/save-point.md, reference/topic-docs.md, and
skills/find-handoff/SKILL.md in the installed cache
(.claude/plugins/cache/melodic-software/session-flow/0.17.14/) are byte-identical to the versions on
main at fdb4dff, so the lines quoted above are the lines that ran.

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

agent-readyFully specified and briefed; eligible for autonomous pickup from the frontier.priority: highSignificant impact, or blocks an imminent release; staff this cycle.work-class: scopedA briefed fix or small feature; blast radius bounded by the brief, tests exist.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions