Build a Cross-Session Handoff

Build a Git-backed handoff protocol for reliable fresh Claude Code sessions.

Introduction

30 Second Summary

A coding session can feel perfectly informed until a new session starts from scratch. Decisions left only in chat can disappear while unfinished code remains.

In this project, you will build Project Relay to prove how durable project state carries decisions across fresh Claude Code sessions. You will capture a repeatable failure before repairing it with repository records and Git evidence.

What You'll Build

A fresh Claude Code session uses repository evidence to repair the webhook processor without access to the previous transcript.

By the end of this project, you'll have:

  • An objective failure record showing why the suffix 42 incorrectly suppresses tenantB:42.
  • A corrected webhook processor that skips exact replays while delivering both tenantA:42 and tenantB:42.
  • A reusable cross-session handoff protocol that lets a fresh agent recover the accepted decision before editing.
  • Secret Mission: Add a deterministic Stop hook that blocks stale handoffs until the project record is updated.

Are there any prerequisites?

You should already be comfortable using Claude Code across multiple sessions. A Mac with Claude Code CLI, Git, and existing Claude access gives you everything else you need at no added cost.

Before We Start

Before the hands-on experiment begins, lock in why Project Relay stores continuity inside the repository. A version-controlled handoff travels with the project, while a resumable conversation depends on one prior transcript.

Set Up the Continuity Lab

Cross-session failures are hard to diagnose when the starting code can change underneath the experiment. A controlled fixture keeps ordinary coding mistakes separate from context loss.

Your Claude Code CLI and Git installation are already confirmed. The remaining foundation is a repository with a valid commit identity.

You will seed a small POSIX shell processor with an unfinished duplicate check. A deterministic test will turn its idempotency contract into evidence.

In this step, get ready to:
  • Initialize the experiment repository with a valid Git identity.
  • Create the fixture directories with ignored runtime state.
  • Checkpoint the executable processor and continuity test in Git.
Initialize the repository

Every commit records an author name and email. Checking these values now keeps the experiment history attributable.

  • Return to the Terminal window from your earlier checks.
  • Display the Git identity that will author your commit by running:
git config user.name
git config user.email

What do these commands check?

The first command prints the author name Git currently uses. The second command prints the matching email.

An empty result means that value needs a repository-local setting after initialization.

  • Create the experiment repository by running these commands:
mkdir -p ~/Projects/claude-state-relay
cd ~/Projects/claude-state-relay
git init

What does this setup create?

  • The first command creates the `claude-state-relay` directory inside `~/Projects`.
  • The second command moves your Terminal session into that directory.
  • The `git init` command creates the empty repository that will hold every experiment checkpoint.

✔️ Both identity values are set

Your commit identity is ready. Git can attach those author details to the checkpoint later in this step.

ⓧ A value is empty

A repository-local identity applies only to this experiment. Fill in both values before creating the first commit.

  • Replace `Your Name` with the name you want recorded in the commit.
  • Replace `you@example.com` with the email you want recorded in the commit.
  • Set both repository-local values by running:
git config user.name "Your Name"
git config user.email "you@example.com"

Where is this identity stored?

These commands write the identity to the current repository configuration. Your other Git repositories keep their existing settings.

Create the fixture structure

The lab separates executable code from architectural records and captured evidence. Generated delivery state belongs outside Git because it changes whenever the processor runs.

  • Create the fixture directories by running:
mkdir -p bin tests docs/decisions evidence

What belongs in each directory?

  • The `bin` directory holds the webhook processor.
  • The `tests` directory holds the continuity contract.
  • The `docs/decisions` directory will hold durable architectural rationale.
  • The `evidence` directory will hold captured test results.
  • Create `.gitignore` with the runtime path by running:
cat > .gitignore <<'EOF'
.runtime/
EOF

Why ignore the runtime directory?

The `.runtime` directory stores generated delivery state. Ignoring it keeps transient results out of the project history.

The quoted `EOF` delimiter preserves the file content literally while the shell writes it to `.gitignore`.

  • Confirm Git can see the new tracked file by running:
git status --short

What should you see?

You should see `.gitignore` listed as an untracked file. Git does not list the empty fixture directories because it tracks files.

Don't see the file?

Check that your Terminal path ends in `Projects/claude-state-relay`. Run the file-creation command again from that directory if needed.

Ask for help with finding why `.gitignore` is missing from Git status.

Write and checkpoint the starter fixture

The processor begins with a visible delivery path and an unfinished duplicate check. The test records the two behaviors that a complete implementation must satisfy.

  • Create `bin/process-webhook.sh` by running:
cat > bin/process-webhook.sh <<'EOF'
#!/bin/sh
set -eu

event_id=${1:?Usage: ./bin/process-webhook.sh <tenant:event-id> [payload]}
payload='{}'
if [ "$#" -ge 2 ]; then
  payload=$2
fi

dedupe_key=${event_id#*:}

mkdir -p .runtime
processed_file=".runtime/processed-events.txt"
deliveries_file=".runtime/deliveries.log"
: >> "$processed_file"
: >> "$deliveries_file"

# TODO: skip events whose dedupe_key was already processed.
printf '%s|%s\n' "$event_id" "$payload" >> "$deliveries_file"
printf 'processed: %s\n' "$event_id"
EOF

What does this processor contain?

  • The `event_id` variable holds the first command argument.
  • The `payload` variable defaults to an empty JSON object.
  • The `dedupe_key` variable extracts the suffix after the colon for the unfinished duplicate check.
  • The `processed_file` and `deliveries_file` variables identify generated runtime records.
  • The TODO leaves duplicate detection unfinished while the delivery path remains executable.
  • Confirm Git can see the processor by running:
git status --short

What changed in the repository?

You should now see the `bin` path alongside `.gitignore`. This confirms that the processor exists inside the repository.

Processor missing from the status?

Check that `bin/process-webhook.sh` sits inside `~/Projects/claude-state-relay`. A different current directory creates the file outside the experiment repository.

Ask for help with locating the missing processor file.

  • Create the exact-duplicate half of `tests/continuity-test.sh` by running:
cat > tests/continuity-test.sh <<'EOF'
#!/bin/sh
set -eu

reset_runtime() {
  rm -rf .runtime
  mkdir -p .runtime
}

delivery_count() {
  count=0
  while IFS= read -r _line; do
    count=$((count + 1))
  done < .runtime/deliveries.log
  printf '%s\n' "$count"
}

reset_runtime
./bin/process-webhook.sh "tenantA:42" '{"amount":10}' > /dev/null
./bin/process-webhook.sh "tenantA:42" '{"amount":10}' > /dev/null
exact_count=$(delivery_count)

if [ "$exact_count" -ne 1 ]; then
  printf 'FAIL: expected an exact duplicate to produce 1 delivery, got %s\n' "$exact_count"
  exit 1
fi
printf 'PASS: exact duplicate produced 1 delivery\n'

EOF

What does the first assertion cover?

The `reset_runtime` function clears delivery state before a scenario. The `delivery_count` function counts recorded delivery lines.

The first scenario expects two identical calls to leave one delivery. A mismatch stops the test with a failure result.

  • Append the tenant-scope assertion to `tests/continuity-test.sh` by running:
cat >> tests/continuity-test.sh <<'EOF'
reset_runtime
first_output=$(./bin/process-webhook.sh "tenantA:42" '{"amount":10}')
second_output=$(./bin/process-webhook.sh "tenantB:42" '{"amount":20}')
tenant_count=$(delivery_count)

if [ "$tenant_count" -ne 2 ]; then
  printf 'FAIL: expected 2 deliveries for tenantA:42 and tenantB:42, got %s\n' "$tenant_count"
  printf 'First result: %s\n' "$first_output"
  printf 'Second result: %s\n' "$second_output"
  exit 1
fi
printf 'PASS: tenant-scoped IDs produced 2 deliveries\n'
EOF

What does the second assertion cover?

The second scenario sends equal local IDs from two tenants. Its count requires both tenant-scoped events to produce deliveries.

The captured `first_output` and `second_output` values make any future failure specific enough to preserve as evidence.

  • Confirm Git can see both fixture scripts by running:
git status --short

What should be listed now?

You should see `.gitignore` with the `bin` and `tests` paths. Those entries confirm that every tracked fixture file exists.

Test file not listed?

Check that the `tests` directory contains `continuity-test.sh`. Recreate the file from both chunks if either command was interrupted.

Ask for help with checking the assembled continuity test.

  • Make both fixture scripts executable by running:
chmod +x bin/process-webhook.sh tests/continuity-test.sh

Why add execute permission?

Execute permission lets the shell run each script directly from its path. Git also records that executable file mode in the commit.

  • Create the initial fixture checkpoint by running:
git add .
git commit -m "Seed partial webhook processor"

What does this checkpoint prove?

The first command stages the tracked fixture content. The second command records that content with the required checkpoint subject.

The repository now has a durable baseline for every later session in the experiment.

Commit did not complete?

Confirm that both Git identity values are set inside `~/Projects/claude-state-relay`. Then run the staging and commit commands again.

Ask for help with diagnosing the failed initial commit.

Before you check the history, what commit subject do you expect to see.

  • Verify the latest checkpoint by running:
git log --oneline -1

What should you see?

The latest line should end with `Seed partial webhook processor`. That subject confirms the repository has the required starting checkpoint.

Your continuity lab now has a stable baseline that fresh sessions can inspect.

✔️ Awesome, I've got everything!

Great. Double check that the latest commit is `Seed partial webhook processor` before moving on.

ⓧ I'd like to double check the full code

Compare each tracked file with these exact versions. The fixture should match them before the next session begins.

.runtime/
#!/bin/sh
set -eu

event_id=${1:?Usage: ./bin/process-webhook.sh <tenant:event-id> [payload]}
payload='{}'
if [ "$#" -ge 2 ]; then
  payload=$2
fi

dedupe_key=${event_id#*:}

mkdir -p .runtime
processed_file=".runtime/processed-events.txt"
deliveries_file=".runtime/deliveries.log"
: >> "$processed_file"
: >> "$deliveries_file"

# TODO: skip events whose dedupe_key was already processed.
printf '%s|%s\n' "$event_id" "$payload" >> "$deliveries_file"
printf 'processed: %s\n' "$event_id"
#!/bin/sh
set -eu

reset_runtime() {
  rm -rf .runtime
  mkdir -p .runtime
}

delivery_count() {
  count=0
  while IFS= read -r _line; do
    count=$((count + 1))
  done < .runtime/deliveries.log
  printf '%s\n' "$count"
}

reset_runtime
./bin/process-webhook.sh "tenantA:42" '{"amount":10}' > /dev/null
./bin/process-webhook.sh "tenantA:42" '{"amount":10}' > /dev/null
exact_count=$(delivery_count)

if [ "$exact_count" -ne 1 ]; then
  printf 'FAIL: expected an exact duplicate to produce 1 delivery, got %s\n' "$exact_count"
  exit 1
fi
printf 'PASS: exact duplicate produced 1 delivery\n'

reset_runtime
first_output=$(./bin/process-webhook.sh "tenantA:42" '{"amount":10}')
second_output=$(./bin/process-webhook.sh "tenantB:42" '{"amount":20}')
tenant_count=$(delivery_count)

if [ "$tenant_count" -ne 2 ]; then
  printf 'FAIL: expected 2 deliveries for tenantA:42 and tenantB:42, got %s\n' "$tenant_count"
  printf 'First result: %s\n' "$first_output"
  printf 'Second result: %s\n' "$second_output"
  exit 1
fi
printf 'PASS: tenant-scoped IDs produced 2 deliveries\n'

That's the foundation in place. Next, you will make the key architectural decision inside a Claude Code conversation without adding it to the repository.

Create a Chat-Only Decision

Your Git repository now gives the experiment a controlled starting point. The processor can accept a webhook while its duplicate-detection TODO waits for an architectural decision.

This step deliberately keeps that decision inside one Claude Code conversation. A later fresh session will have the code without the rationale or planned repair.

In this step, get ready to:
  • Start a fresh Claude Code session inside the seeded repository.
  • Keep the full-ID architecture decision in conversation only.
  • Prove one event works before closing the session.
Establish the chat-only constraint

A fresh interactive session can inspect the repository from its current state. Your prompt will establish the architectural requirement while preventing Claude from recording it in a file.

  • Start a fresh interactive Claude Code session from the repository by running:
claude

Claude Code does not start?

The claude command starts an interactive session in the current repository. If your terminal cannot find the command, verify that the installed CLI is available from this terminal.

Help me diagnose why Claude Code does not start from my project repository.

  • Set the architectural constraint without changing the repository by sending this exact prompt:
Inspect the webhook processor and test. The architecture requirement is that tenantA:42 and tenantB:42 are distinct opaque event IDs. The complete event_id must be the idempotency key. Explain why the current suffix-only dedupe_key is temporary and what must change next, but do not edit files or create documentation.

What is this prompt testing?

The prompt asks Claude to distinguish transitional code from an architectural requirement. The current suffix expression removes the tenant prefix.

The final sentence forbids edits or documentation. That keeps the rationale inside the conversation instead of the repository.

Claude should identify ${event_id#*:} as the unsafe expression. It should explain that the complete event_id must replace the suffix-only key during the next implementation change.

Demonstrate one successful delivery

An idempotency key identifies whether an event has already been handled. The complete tenant-scoped ID preserves the difference between events that share the same local suffix.

  • Confirm Claude identifies dedupe_key=${event_id#*:} as unsafe.
  • Confirm Claude explains that tenantA:42 reduces to 42.
  • Confirm Claude explains that tenantB:42 also reduces to 42.
  • Leave the repository untouched after Claude finishes its explanation.

Why does the full ID matter?

The values tenantA:42 and tenantB:42 represent distinct events. Using only 42 makes the processor treat them as one event.

The test fixture preserves this distinction as an observable requirement. A future session needs the missing rationale to interpret the temporary variable correctly.

Before Claude runs the processor, do you expect one new event to be processed or skipped?

  • Ask Claude to run the one-event verification by sending this command:
./bin/process-webhook.sh "tenantA:42" '{"amount":10}'

What does this command prove?

The command passes one tenant-scoped event ID with a small payload to the processor. It proves the starter script can record a delivery.

This single event does not exercise duplicate detection. The later continuity test supplies that stronger check.

You should see processed: tenantA:42 in Claude's terminal output. Your first delivery now works.

The processor can handle one event while the tenant-scope decision remains unrecorded.

Do not see the processed output?

Check whether Claude executed the command or only explained it. Ask Claude to run the exact command if no terminal output appears.

Help me understand why the webhook command did not print the expected processed output.

Seal the decision in conversation

The context boundary becomes real when the first session ends. Closing it now leaves the decision in the transcript without adding a durable project artifact.

  • Exit the current Claude Code session by entering:
/exit

What does this command do?

The /exit command closes the current interactive Claude Code session. Your terminal returns to its normal shell prompt.

The conversation no longer supplies active context. The repository remains the only project state available to a genuinely fresh session.

  • Confirm your normal terminal prompt is visible again.
  • Keep the next session separate by leaving this conversation closed.

Before checking the repository, do you expect this chat-only session to leave any tracked file changes?

  • Confirm the session left the tracked repository untouched by running:
git status --short

What does this check prove?

This command displays tracked working-tree changes in compact form. Silence means the chat-only session did not alter the committed project files.

You should see no output. The Seed partial webhook processor checkpoint remains unchanged.

Does Git list a change?

A listed source file or document means the session changed tracked repository state. Restore only those unexpected changes before continuing so the decision remains chat-only.

Help me inspect unexpected Git changes from a chat-only Claude Code session.

The architectural decision is now stranded inside a closed conversation. Next, a fresh session will see the temporary suffix key without the rationale that explains why it must change.

Capture the Fresh-Session Failure

Your first Claude Code session proved that tenantA:42 can be processed. Its idempotency key decision still lives only in the previous conversation.

A genuinely fresh session starts with a new context window. This step tests what it infers from the visible TODO plus the existing dedupe_key.

In this step, get ready to:
  • Start a genuinely fresh session with a constrained handoff prompt.
  • Observe its implementation under the continuity test.
  • Preserve the failure as version-controlled evidence.
Start a genuinely fresh session
  • Start a fresh Claude Code session from the repository by running this command:
claude

What does this command do?

The claude command starts an interactive session with a fresh context. Commands such as claude -c or claude -r would restore an earlier conversation.

Avoid /resume during this experiment. The session must rely on repository state alone.

  • Predict whether visible code alone can preserve the earlier architecture decision.
  • Send this exact prompt to constrain the fresh session:
Continue from the current working tree. Finish only the duplicate-detection TODO in bin/process-webhook.sh using the existing dedupe_key and current data flow. Do not redesign completed code. Run ./tests/continuity-test.sh when finished.

What does this prompt control?

The prompt limits the task to the visible duplicate-detection TODO. It directs Claude to preserve the existing dedupe_key data flow.

The continuity test then checks the implementation against both duplicate scenarios. This turns the missing context into observable evidence.

Observe the suffix-only failure

Claude completes the TODO before running tests/continuity-test.sh. You will see the exact-duplicate assertion pass first.

The tenant-scoped assertion then reports FAIL: expected 2 deliveries for tenantA:42 and tenantB:42, got 1. Its second result is duplicate skipped: tenantB:42.

  • Inspect Claude's edit to bin/process-webhook.sh.
  • Confirm the new loop compares saved_id with dedupe_key.
  • Confirm the new write appends dedupe_key to processed_file.

Why did the tenant test fail?

The expression dedupe_key=${event_id#*:} strips the tenant prefix. Both sample event IDs become 42.

The new loop can skip an exact duplicate. It also collapses two distinct tenant events onto the same saved value.

This shortfall is intentional. The test has exposed the architectural knowledge that failed to cross the session boundary.

Preserve the failure as evidence

Terminal output disappears when a session ends. A Git checkpoint keeps the incorrect executable state beside the test result that disproves it.

  • Exit the fresh Claude Code session by entering:
/exit

What does this command do?

The /exit command closes the interactive Claude Code session. You return to the shell without resuming its context.

  • Capture the known test failure in evidence/baseline-failure.txt by running this command:
./tests/continuity-test.sh > evidence/baseline-failure.txt 2>&1 || true

What does this command capture?

The redirection saves the test output in evidence/baseline-failure.txt. It includes both the passing assertion plus the intended failure.

The final || true keeps the shell available after the test returns its expected failing status.

  • Open evidence/baseline-failure.txt in your text editor.
  • Find FAIL: expected 2 deliveries for tenantA:42 and tenantB:42, got 1 in the saved output.
  • Find duplicate skipped: tenantB:42 in the saved output.

The file now proves that exact duplicate detection works. It also proves that suffix-only deduplication suppresses a distinct tenant event.

  • Stage the incorrect processor plus its failure evidence by running:
git add bin/process-webhook.sh evidence/baseline-failure.txt

What does this command stage?

This stages the suffix-only implementation plus evidence/baseline-failure.txt for the same checkpoint. The code plus its disproving evidence remain linked.

  • Commit the failed baseline by running:
git commit -m "Capture suffix-only dedupe failure"

What does this checkpoint preserve?

The commit records the fresh session's suffix-only implementation. It also records the continuity test result that exposes the mistake.

  • Predict what a clean checkpoint should show in status plus recent history.
  • Check the working tree by running:
git status --short

What should the status show?

A clean working tree produces no status entries. This confirms that the processor plus the evidence are committed.

  • Inspect the three most recent commits by running:
git log --oneline -3

What should the history show?

The latest subject should be Capture suffix-only dedupe failure. The seed commit remains below it in the history.

You have captured the hard part: Git now preserves the exact context-loss failure for another session to inspect.

✔️ Awesome, I've got everything!

Your failed baseline is saved. Double check that the working tree is clean before moving on.

ⓧ I'd like to double check the full code

Compare these files with your repository. They show the exact committed state at this checkpoint.

.runtime/

Generated delivery state remains outside the committed experiment evidence.

#!/bin/sh
set -eu

event_id=${1:?Usage: ./bin/process-webhook.sh <tenant:event-id> [payload]}
payload='{}'
if [ "$#" -ge 2 ]; then
  payload=$2
fi

dedupe_key=${event_id#*:}

mkdir -p .runtime
processed_file=".runtime/processed-events.txt"
deliveries_file=".runtime/deliveries.log"
: >> "$processed_file"
: >> "$deliveries_file"

seen=false
while IFS= read -r saved_id; do
  if [ "$saved_id" = "$dedupe_key" ]; then
    seen=true
    break
  fi
done < "$processed_file"

if [ "$seen" = "true" ]; then
  printf 'duplicate skipped: %s\n' "$event_id"
  exit 0
fi

printf '%s\n' "$dedupe_key" >> "$processed_file"
printf '%s|%s\n' "$event_id" "$payload" >> "$deliveries_file"
printf 'processed: %s\n' "$event_id"

This processor intentionally compares plus persists the suffix-only dedupe_key at this checkpoint.

#!/bin/sh
set -eu

reset_runtime() {
  rm -rf .runtime
  mkdir -p .runtime
}

delivery_count() {
  count=0
  while IFS= read -r _line; do
    count=$((count + 1))
  done < .runtime/deliveries.log
  printf '%s\n' "$count"
}

reset_runtime
./bin/process-webhook.sh "tenantA:42" '{"amount":10}' > /dev/null
./bin/process-webhook.sh "tenantA:42" '{"amount":10}' > /dev/null
exact_count=$(delivery_count)

if [ "$exact_count" -ne 1 ]; then
  printf 'FAIL: expected an exact duplicate to produce 1 delivery, got %s\n' "$exact_count"
  exit 1
fi
printf 'PASS: exact duplicate produced 1 delivery\n'

reset_runtime
first_output=$(./bin/process-webhook.sh "tenantA:42" '{"amount":10}')
second_output=$(./bin/process-webhook.sh "tenantB:42" '{"amount":20}')
tenant_count=$(delivery_count)

if [ "$tenant_count" -ne 2 ]; then
  printf 'FAIL: expected 2 deliveries for tenantA:42 and tenantB:42, got %s\n' "$tenant_count"
  printf 'First result: %s\n' "$first_output"
  printf 'Second result: %s\n' "$second_output"
  exit 1
fi
printf 'PASS: tenant-scoped IDs produced 2 deliveries\n'

The continuity test remains unchanged. It distinguishes exact replays from equal local IDs owned by different tenants.

PASS: exact duplicate produced 1 delivery
FAIL: expected 2 deliveries for tenantA:42 and tenantB:42, got 1
First result: processed: tenantA:42
Second result: duplicate skipped: tenantB:42

This evidence file records the exact baseline failure produced by the fresh session.

The failure is now reproducible. Next, you will externalize the missing rationale into durable project state.

Externalize the Project State

Your fresh-session experiment now has a committed failure. Suffix-only deduplication skips a valid event from the second tenant.

Code shows the implementation's behavior. An architectural decision record preserves durable rationale.

A handoff tracks current progress. Git preserves committed evidence.

This step gives each kind of project state its own repository-owned artifact.

In this step, get ready to:
  • Preserve the full-ID decision in an accepted architectural record.
  • Capture the current defect in a mutable handoff.
  • Define the repository-owned session contract.
Record the full-ID decision

The architectural record owns the stable decision. Future sessions can use its accepted rationale even when transitional code points in the wrong direction.

  • Create docs/decisions/ADR-001-idempotency-key.md inside the existing ~/Projects/claude-state-relay repository using your terminal editor.
  • Paste the following architectural record into the file:
# ADR-001: Preserve the Full Webhook Event ID

Status: Accepted

## Context

Webhook event IDs are opaque, tenant-scoped values. The local suffix can repeat across tenants. For example, `tenantA:42` and `tenantB:42` describe two distinct events even though both end in `42`.

A transitional implementation derives `${event_id#*:}`. Using that suffix as the deduplication key collapses distinct tenant events and can suppress a valid delivery.

## Decision

Use the complete `event_id` as the idempotency key. Compare and persist the exact value without removing the tenant prefix.

## Consequences

- An exact replay of `tenantA:42` is skipped.
- `tenantA:42` and `tenantB:42` are both delivered.
- Any suffix-only variable is transitional code, not an architectural decision.

What does this record preserve?

  • The Accepted status identifies the decision as the current architectural rule.
  • The context records how local suffixes collide across tenants.
  • The consequences define the behavior that the continuity test must prove.
  • Save docs/decisions/ADR-001-idempotency-key.md.
  • Check whether Git recognizes the new record by running this command:
git status --short

What does this check prove?

The short status shows files that differ from the latest commit. Seeing the ADR path confirms that the record exists inside the repository.

You should see docs/decisions/ADR-001-idempotency-key.md listed as a new untracked file.

ADR file missing from Git status?

  • Confirm your terminal editor saved the file inside ~/Projects/claude-state-relay/docs/decisions.
  • Check that the filename uses the exact capitalization shown above.

help me find why my ADR is missing from Git status.

Capture the mutable handoff

The handoff owns the project's changing execution state. It records the current defect plus one exact action for the next session.

  • Create PROJECT_HANDOFF.md inside ~/Projects/claude-state-relay using your terminal editor.
  • Start the handoff with this content:
# Project Handoff

Status: In Progress

## Current Goal

Preserve correct webhook idempotency across fresh Claude Code sessions.

## Completed

- Reproduced the suffix-only deduplication failure.
- Added the cross-session operating contract and accepted ADR.

## Architectural Constraint

The complete `event_id` is the idempotency key. Do not derive or persist `${event_id#*:}` for duplicate detection.

## Current Implementation

`bin/process-webhook.sh` currently derives `dedupe_key=${event_id#*:}` and compares and persists that suffix. This incorrectly treats `tenantA:42` and `tenantB:42` as duplicates.

What does this section capture?

  • The current goal keeps the handoff focused on cross-session idempotency.
  • The architectural constraint agrees with the accepted ADR.
  • The current implementation describes the suffix-only defect without hiding it.
  • Save PROJECT_HANDOFF.md.
  • Confirm Git detects the handoff by running this command:
git status --short

What changed in the repository?

Git now tracks two uncommitted documentation additions. The status separates these new records from the already committed failing implementation.

You should see paths for the ADR and PROJECT_HANDOFF.md in the output.

  • Add the remaining sections directly below the current implementation paragraph:
## Verification

Command: `./tests/continuity-test.sh`

Evidence: `evidence/baseline-failure.txt`

## Blockers

None.

## Next Action

Replace the suffix-only dedupe key with the complete `event_id`, rerun the continuity test, save passing output to `evidence/final-success.txt`, update this handoff, and commit the completed checkpoint.

Why does the handoff end here?

The verification section points to the repeatable test plus its recorded failure. The next-action section gives a fresh session one bounded repair to perform.

  • Save the completed PROJECT_HANDOFF.md.
  • Confirm the completed handoff remains visible to Git by running this command:
git status --short

What should Git show now?

The status should still list the ADR and handoff as new files. Their presence confirms that the durable decision and mutable checkpoint are ready for the session contract.

The existing bin/process-webhook.sh should remain absent from the status output because this step does not repair the defect.

Handoff content incomplete?

  • Check that PROJECT_HANDOFF.md contains the current implementation paragraph.
  • Check that the final section names exactly one next action.
  • Confirm the evidence path is evidence/baseline-failure.txt.

help me compare my handoff with the required checkpoint.

Define the startup contract

A project-level CLAUDE.md supplies operating rules to each fresh Claude Code session. This contract makes rehydration plus checkpointing part of the normal workflow.

  • Create CLAUDE.md inside ~/Projects/claude-state-relay using your terminal editor.
  • Paste the following operating contract into the file:
# Cross-Session Operating Contract

## Rehydrate Before Editing

1. Read @docs/decisions/ADR-001-idempotency-key.md.
2. Read @PROJECT_HANDOFF.md.
3. Run `git status --short` and `git log --oneline -3`.
4. Before editing, report the accepted architectural constraint, current implementation state, latest evidence, and exact next action.

## Checkpoint Before Stopping

1. Run the verification command recorded in `PROJECT_HANDOFF.md`.
2. Update `PROJECT_HANDOFF.md` with completed work, observed evidence, remaining blockers, and exactly one next action.
3. Commit code and handoff changes together so Git history supports the written state.
4. Never infer an architectural decision from transitional code when an accepted ADR covers it.

How does the contract work?

  • The rehydration rules require the ADR and handoff to be read before an edit.
  • The Git checks expose the current working tree plus recent evidence.
  • The checkpoint rules keep verified code changes aligned with the mutable handoff.
  • Save CLAUDE.md.
  • Confirm Git sees all three protocol files by running this command:
git status --short

What does the status confirm?

Git should list CLAUDE.md alongside the ADR and handoff. These three files now cover operating rules, durable rationale, and mutable progress.

You should see all three new protocol paths in the short status output.

✔️ Awesome, I've got everything!

Your ADR, handoff, and startup contract are ready to checkpoint.

ⓧ I'd like to double check the full code

These are the exact contents of the three project-state files at this checkpoint.

# ADR-001: Preserve the Full Webhook Event ID

Status: Accepted

## Context

Webhook event IDs are opaque, tenant-scoped values. The local suffix can repeat across tenants. For example, `tenantA:42` and `tenantB:42` describe two distinct events even though both end in `42`.

A transitional implementation derives `${event_id#*:}`. Using that suffix as the deduplication key collapses distinct tenant events and can suppress a valid delivery.

## Decision

Use the complete `event_id` as the idempotency key. Compare and persist the exact value without removing the tenant prefix.

## Consequences

- An exact replay of `tenantA:42` is skipped.
- `tenantA:42` and `tenantB:42` are both delivered.
- Any suffix-only variable is transitional code, not an architectural decision.

ADR checkpoint

This file accepts the complete event ID as the durable idempotency decision.

The mutable checkpoint should match this full PROJECT_HANDOFF.md file.

# Project Handoff

Status: In Progress

## Current Goal

Preserve correct webhook idempotency across fresh Claude Code sessions.

## Completed

- Reproduced the suffix-only deduplication failure.
- Added the cross-session operating contract and accepted ADR.

## Architectural Constraint

The complete `event_id` is the idempotency key. Do not derive or persist `${event_id#*:}` for duplicate detection.

## Current Implementation

`bin/process-webhook.sh` currently derives `dedupe_key=${event_id#*:}` and compares and persists that suffix. This incorrectly treats `tenantA:42` and `tenantB:42` as duplicates.

## Verification

Command: `./tests/continuity-test.sh`

Evidence: `evidence/baseline-failure.txt`

## Blockers

None.

## Next Action

Replace the suffix-only dedupe key with the complete `event_id`, rerun the continuity test, save passing output to `evidence/final-success.txt`, update this handoff, and commit the completed checkpoint.

Handoff checkpoint

This file records the known defect plus the exact repair for the next fresh session.

The startup rules should match this full CLAUDE.md file.

# Cross-Session Operating Contract

## Rehydrate Before Editing

1. Read @docs/decisions/ADR-001-idempotency-key.md.
2. Read @PROJECT_HANDOFF.md.
3. Run `git status --short` and `git log --oneline -3`.
4. Before editing, report the accepted architectural constraint, current implementation state, latest evidence, and exact next action.

## Checkpoint Before Stopping

1. Run the verification command recorded in `PROJECT_HANDOFF.md`.
2. Update `PROJECT_HANDOFF.md` with completed work, observed evidence, remaining blockers, and exactly one next action.
3. Commit code and handoff changes together so Git history supports the written state.
4. Never infer an architectural decision from transitional code when an accepted ADR covers it.

Operating-contract checkpoint

This file requires source-by-source rehydration before editing plus a verified checkpoint before stopping.

  • Stage the three state files and create their checkpoint by running:
git add CLAUDE.md PROJECT_HANDOFF.md docs/decisions/ADR-001-idempotency-key.md
git commit -m "Add cross-session state protocol"

What does this checkpoint preserve?

The first command stages the three repository-owned state files. The second command commits them under one shared checkpoint subject.

Git should print a new commit summary containing Add cross-session state protocol.

Commit did not complete?

  • Confirm all three files are saved in ~/Projects/claude-state-relay.
  • Confirm the repository-local Git identity from the setup step is still configured.
  • Check that your terminal remains inside the experiment repository.

help me troubleshoot the state-protocol commit.

Before you check, do you expect Git to report any uncommitted files?

  • Verify the clean checkpoint and recent commit order by running:
git status --short
git log --oneline -2

What should you see?

The first command should print no status entries. The log should place Add cross-session state protocol above Capture suffix-only dedupe failure.

That is the continuity layer in place: your repository now carries its operating rules, architectural rationale, current progress, and Git evidence. Next, you'll start a fresh Claude Code session and test whether those artifacts guide the repair.

Rehydrate and Repair the Session

Your Git repository now preserves the operating rules, architectural rationale, current handoff, and failure evidence. A final fresh Claude Code session has to prove those layers are enough.

Fresh sessions begin with a new context boundary. This session must use state rehydration to replace the suffix-only key without repeating the previous mistake.

In this step, get ready to:
  • Verify that a fresh session loads the project operating contract.
  • Require a source-by-source report before the repair begins.
  • Prove the repaired implementation with a passing committed checkpoint.
Start a fresh session

A fresh session removes the previous transcript from the experiment. The repository must supply every fact Claude needs before editing.

  • Start a fresh Claude Code session from the repository from earlier by running this command:
claude

What does this command prove?

The plain claude command starts an interactive session. The previous conversation does not supply the missing rationale.

  • Keep the previous chats out of the fresh session.
  • Check which memory files loaded by entering this slash command:
/context

What should you see?

You should see the project CLAUDE.md beneath Memory files. This confirms that the fresh session received the repository-owned startup contract.

Project memory missing?

  • End the current session.
  • Return to ~/Projects/claude-state-relay in the terminal from earlier.
  • Start a new fresh session with the command above.

Get help checking why CLAUDE.md did not load.

Require source-by-source rehydration

Each durable artifact answers a different recovery question. The startup contract supplies the operating rule while the accepted architectural decision record supplies the missing rationale.

The mutable handoff identifies the exact next action. Git history provides evidence that the recorded state matches completed work.

  • Give the fresh session the complete recovery contract by sending this exact prompt:
Rehydrate this project before editing. Report the operating rule from CLAUDE.md, the accepted decision and rationale from ADR-001, the current next action from PROJECT_HANDOFF.md, and the latest relevant Git evidence. Then perform exactly that next action. Save the passing test output to evidence/final-success.txt, update PROJECT_HANDOFF.md, and commit the completed checkpoint.

How does this prompt control the repair?

  • The opening sentence makes repository inspection a gate before editing.
  • The source report exposes whether Claude understood each artifact.
  • The exact next action keeps the repair aligned with PROJECT_HANDOFF.md.
  • The final sentence requires passing evidence, an updated handoff, and a Git checkpoint.
  • Review Claude's source report before it edits any file.
  • Confirm the operating rule requires verification, a handoff update, and a committed checkpoint.
  • Confirm the accepted decision uses the complete event_id as the idempotency key.
  • Confirm the rationale identifies tenant suffix collisions as the defect.
  • Confirm the handoff names the replacement of dedupe_key with event_id as the next action.
  • Confirm the Git evidence includes the state-protocol commit above the failure commit.

Claude should state the full-ID constraint before its first edit. That report proves the session recovered intent from durable project state.

Verify the repaired checkpoint

A handoff becomes trustworthy when the implementation, test evidence, and Git history agree. The final check tests all three signals from the repository from earlier.

  • Review Claude's completion summary.
  • Confirm bin/process-webhook.sh compares and persists event_id exactly.
  • Confirm evidence/final-success.txt records both passing assertions.
  • Confirm PROJECT_HANDOFF.md reports Status: Complete.

Before you run the final check, do you expect the working tree to contain any uncommitted entries?

  • Verify the tests, working tree, and latest checkpoint by running this command:
./tests/continuity-test.sh && git status --short && git log --oneline -3

What should you see?

  • The first test line should read PASS: exact duplicate produced 1 delivery.
  • The second test line should read PASS: tenant-scoped IDs produced 2 deliveries.
  • The short status command should print no entries.
  • The latest commit subject should be Fix dedupe key after state rehydration.

Checkpoint does not match?

  • Ask Claude to compare the saved identifier with event_id if either test fails.
  • Ask Claude to include every changed file in the completed checkpoint if the short status has entries.
  • Ask Claude to inspect the latest commit if the expected subject is missing.

Get help diagnosing the final checkpoint.

You have proven the continuity contract under a genuine context boundary. A fresh session recovered the architecture, repaired the defect, and left the next session a clean checkpoint.

✔️ Awesome, I've got everything!

Your continuity test passes. Your handoff and Git history now describe the same completed state.

ⓧ I'd like to double check the full code

Compare each repository artifact below with your final checkpoint.

.runtime/

Why keep this file?

This rule keeps generated delivery state outside Git evidence. Each test run can reset .runtime/ without dirtying the checkpoint.

# Cross-Session Operating Contract

## Rehydrate Before Editing

1. Read @docs/decisions/ADR-001-idempotency-key.md.
2. Read @PROJECT_HANDOFF.md.
3. Run `git status --short` and `git log --oneline -3`.
4. Before editing, report the accepted architectural constraint, current implementation state, latest evidence, and exact next action.

## Checkpoint Before Stopping

1. Run the verification command recorded in `PROJECT_HANDOFF.md`.
2. Update `PROJECT_HANDOFF.md` with completed work, observed evidence, remaining blockers, and exactly one next action.
3. Commit code and handoff changes together so Git history supports the written state.
4. Never infer an architectural decision from transitional code when an accepted ADR covers it.

Why keep the operating contract?

This file defines how every fresh session starts and finishes. It turns rehydration and checkpointing into repeatable project rules.

# ADR-001: Preserve the Full Webhook Event ID

Status: Accepted

## Context

Webhook event IDs are opaque, tenant-scoped values. The local suffix can repeat across tenants. For example, `tenantA:42` and `tenantB:42` describe two distinct events even though both end in `42`.

A transitional implementation derives `${event_id#*:}`. Using that suffix as the deduplication key collapses distinct tenant events and can suppress a valid delivery.

## Decision

Use the complete `event_id` as the idempotency key. Compare and persist the exact value without removing the tenant prefix.

## Consequences

- An exact replay of `tenantA:42` is skipped.
- `tenantA:42` and `tenantB:42` are both delivered.
- Any suffix-only variable is transitional code, not an architectural decision.

Why keep the ADR?

The ADR preserves the accepted full-ID decision with its collision rationale. Future sessions can distinguish architecture from transitional code.

#!/bin/sh
set -eu

event_id=${1:?Usage: ./bin/process-webhook.sh <tenant:event-id> [payload]}
payload='{}'
if [ "$#" -ge 2 ]; then
  payload=$2
fi

mkdir -p .runtime
processed_file=".runtime/processed-events.txt"
deliveries_file=".runtime/deliveries.log"
: >> "$processed_file"
: >> "$deliveries_file"

seen=false
while IFS= read -r saved_id; do
  if [ "$saved_id" = "$event_id" ]; then
    seen=true
    break
  fi
done < "$processed_file"

if [ "$seen" = "true" ]; then
  printf 'duplicate skipped: %s\n' "$event_id"
  exit 0
fi

printf '%s\n' "$event_id" >> "$processed_file"
printf '%s|%s\n' "$event_id" "$payload" >> "$deliveries_file"
printf 'processed: %s\n' "$event_id"

How does the repair work?

The seen loop compares each saved value with the complete event_id. New deliveries persist that same complete value.

An exact replay is skipped while equal local suffixes from different tenants remain distinct.

#!/bin/sh
set -eu

reset_runtime() {
  rm -rf .runtime
  mkdir -p .runtime
}

delivery_count() {
  count=0
  while IFS= read -r _line; do
    count=$((count + 1))
  done < .runtime/deliveries.log
  printf '%s\n' "$count"
}

reset_runtime
./bin/process-webhook.sh "tenantA:42" '{"amount":10}' > /dev/null
./bin/process-webhook.sh "tenantA:42" '{"amount":10}' > /dev/null
exact_count=$(delivery_count)

if [ "$exact_count" -ne 1 ]; then
  printf 'FAIL: expected an exact duplicate to produce 1 delivery, got %s\n' "$exact_count"
  exit 1
fi
printf 'PASS: exact duplicate produced 1 delivery\n'

reset_runtime
first_output=$(./bin/process-webhook.sh "tenantA:42" '{"amount":10}')
second_output=$(./bin/process-webhook.sh "tenantB:42" '{"amount":20}')
tenant_count=$(delivery_count)

if [ "$tenant_count" -ne 2 ]; then
  printf 'FAIL: expected 2 deliveries for tenantA:42 and tenantB:42, got %s\n' "$tenant_count"
  printf 'First result: %s\n' "$first_output"
  printf 'Second result: %s\n' "$second_output"
  exit 1
fi
printf 'PASS: tenant-scoped IDs produced 2 deliveries\n'

What does the test prove?

The first assertion requires an exact replay to produce one delivery. The second requires equal local IDs from separate tenants to produce two deliveries.

# Project Handoff

Status: Complete

## Current Goal

Preserve correct webhook idempotency across fresh Claude Code sessions.

## Completed

- Reproduced the suffix-only deduplication failure.
- Added the cross-session operating contract and accepted ADR.
- Rehydrated a fresh session from repository state.
- Replaced suffix-only deduplication with exact full-ID comparison.
- Saved passing verification evidence.

## Architectural Constraint

The complete `event_id` is the idempotency key. Do not derive or persist `${event_id#*:}` for duplicate detection.

## Verification

Command: `./tests/continuity-test.sh`

Expected result:

- `PASS: exact duplicate produced 1 delivery`
- `PASS: tenant-scoped IDs produced 2 deliveries`

Evidence: `evidence/final-success.txt`

## Git Evidence

`git log --oneline -3` includes:

- `Fix dedupe key after state rehydration`
- `Add cross-session state protocol`
- `Capture suffix-only dedupe failure`

## Blockers

None.

## Next Action

None. The continuity experiment is complete.

Why update the handoff?

The handoff now records completed work, passing evidence, and the latest relevant commits. Its next action shows that the continuity experiment has reached a stable endpoint.

PASS: exact duplicate produced 1 delivery
FAIL: expected 2 deliveries for tenantA:42 and tenantB:42, got 1
First result: processed: tenantA:42
Second result: duplicate skipped: tenantB:42

Why preserve the baseline?

This evidence records the exact failure caused by suffix-only deduplication. It provides an objective comparison for the repaired checkpoint.

PASS: exact duplicate produced 1 delivery
PASS: tenant-scoped IDs produced 2 deliveries

Why preserve the final result?

This evidence proves the repaired processor satisfies both idempotency requirements. The fresh session has converted recovered project state into a verified result.

Secret mission

Add a Handoff Freshness Guard

Your repository now carries enough state for a fresh session to continue correctly. Add a project-scoped Stop hook that blocks Claude when source or test changes are missing a matching handoff update.

Clean Up Your Resources

Clean Up Your Resources

Everything in this project stays on your Mac, so there are no cloud resources or ongoing infrastructure costs. Choose whether to keep the repository, pause your work, or delete the local project state.

Resources you used:

  • The local Git repository at ~/Projects/claude-state-relay.
  • The generated delivery state under .runtime/ inside the repository.
  • The local Claude Code project state associated with the repository.

Keep everything running

No action is needed. Choose this if you want to reuse the repository as a cross-session handoff template.

  • Keep ~/Projects/claude-state-relay on your Mac.
  • Retain the local Claude Code project state if you want its existing session history available.
  • Reuse the committed protocol when you begin another multi-session experiment.

Pause - I'll come back to this later

Close the current Claude Code session while preserving every committed project artifact. The repository remains ready for another fresh session.

  • Return to the Claude Code session from earlier.
  • Enter /exit to close the session.
  • Leave ~/Projects/claude-state-relay in place.
  • Start a fresh Claude Code session from the same repository when you return.

Delete - I don't want to use this again

Deleting the local project state and repository is permanent. The dry run lets you inspect the target before anything is removed.

  • Exit the Claude Code session from earlier by entering /exit.
  • Preview removal of the repository's local Claude Code project state by running this command in your terminal:
claude project purge ~/Projects/claude-state-relay --dry-run

What Does the Preview Do?

The dry run shows which local Claude Code project state is targeted. It leaves that state in place.

  • Confirm the preview targets ~/Projects/claude-state-relay.
  • Remove the associated local Claude Code project state by running:
claude project purge ~/Projects/claude-state-relay

What Does the Purge Remove?

This removes Claude Code's local project state for the repository. The Git repository remains on your Mac.

  • Read the deletion target in the next command.
  • Confirm the target is exactly ~/Projects/claude-state-relay.
  • Delete the repository and its generated runtime files by running:
rm -rf ~/Projects/claude-state-relay

What Does This Delete?

This removes the complete local repository. It also removes the ignored .runtime/ directory inside it.

  • Confirm the deletion with an expected directory lookup failure by running:
cd ~/Projects/claude-state-relay

What Should You See?

The command should fail because the directory no longer exists. That failure confirms the repository was removed.

Nice Work!

Nice Work!

Brilliant work! Your version-controlled continuity protocol now lets a fresh Claude Code session recover the project state without a previous transcript.

You've learned how to:

  • Reproduce a fresh-session continuity failure where suffix-only deduplication skips tenantB:42. Preserve the failed test in evidence/baseline-failure.txt.
  • Build a repository-owned continuity protocol from four durable layers. CLAUDE.md holds operating rules. ADR-001-idempotency-key.md holds architectural rationale. PROJECT_HANDOFF.md holds current progress. Git history preserves evidence.
  • Use state rehydration to guide a fresh Claude Code session. The session repaired the full event_id key. Passing evidence in evidence/final-success.txt proves the handoff worked.
  • Secret Mission: Added a deterministic Stop hook that catches source or test changes without a matching PROJECT_HANDOFF.md update. Loop protection through stop_hook_active lets Claude stop after the checkpoint is repaired.

Ready to quiz yourself?