Long AI coding sessions have a quiet failure mode.
They do not always fail with a bad command, a broken test, or an obvious wrong turn.
Sometimes they fail at the boundary between one context window and the next.
That boundary is exactly where I do not like giving control to an opaque automatic compaction step.
Most AI coding tools eventually hit the same problem. The conversation gets long, the context window gets full, and something needs to happen before the next step becomes unreliable.
The default answer is usually some form of /compact.
I understand why it exists. A long transcript cannot grow forever.
But I never liked the control model.
The assistant decides what matters. It rewrites the conversation into a smaller representation. Then I continue from that summary, without really knowing what was dropped, what was compressed too aggressively, or which failed attempt is now missing from the working memory.
That is exactly the kind of hidden state I try to avoid in coding workflows.
My first serious answer to this was handoff, a local-first CLI for structured AI coding sessions.
That still works well when I want a spec-driven flow: intent, spec, design, decisions, state, session summary, and drift checks.
But not every workday task deserves that much process.
Sometimes I am already deep in a repository, moving through small implementation steps, and I only need one thing:
Before the session gets cleared, write down exactly enough state for the next session to continue safely.
So I built a lighter setup around Claude Code.
What I wanted
The goal was not to create another full workflow system.
I wanted a small project-local rollover protocol:
- disable automatic compaction
- show context usage while I work
- warn when context gets high
- write a concrete
HANDOFF.mdbefore clearing - load that handoff automatically in the fresh session
- keep everything inspectable as normal files
The important part is that the handoff is explicit.
I can open it, edit it, delete it, review it, or decide it is not good enough. The next session starts from a file I control, not from a hidden summary I have to trust.
The project layout is intentionally small:
.claude/
settings.json
skills/
handoff/
SKILL.md
templates/
HANDOFF.template.md
scripts/
context_monitor.py
load_handoff.py
statusline.py
The Claude settings
The first decision is to turn off auto compaction.
In .claude/settings.json:
{
"autoCompactEnabled": false,
"env": {
"CLAUDE_CONTEXT_WINDOW": "1000000",
"CLAUDE_ROLLOVER_WARN_BANDS": "75,85,92"
},
"statusLine": {
"type": "command",
"command": "python3 .claude/scripts/statusline.py"
},
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/scripts/context_monitor.py\""
}
]
}
],
"PostToolUse": [
{
"hooks": [
{
"type": "command",
"command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/scripts/context_monitor.py\""
}
]
}
],
"SessionStart": [
{
"matcher": "startup|clear",
"hooks": [
{
"type": "command",
"command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/scripts/load_handoff.py\""
}
]
}
]
}
}
There are three moving parts here.
statusline.py keeps the current model, branch, and context usage visible while I work.
context_monitor.py runs after user prompts and tool use. When context usage crosses a configured band, it tells the session that rollover is recommended.
load_handoff.py runs when a new session starts or after /clear. If HANDOFF.md exists at the project root, it injects it into the new session as additional context.
That is the loop.
The context warning
The monitor is deliberately boring.
It reads the session transcript path from hook input, scans only the tail of the file, finds the latest main-chain assistant usage entry, and computes an approximate percentage against the context window.
When the percentage crosses a band, it warns once for that band:
msg = (
f"Context at ~{pct}% ({tokens:,}/{window:,} tokens). "
"Rollover recommended: run /handoff, then /clear."
)
note = (
f"[context-rollover] Context usage is at ~{pct}% of the window. "
"Per this project's rollover protocol: finish the current atomic step "
"(do not leave the working tree broken), then tell the user it is time "
"to roll over and offer to run /handoff to write HANDOFF.md. "
"Do NOT suggest /compact."
)
That last line matters.
I do not want the assistant to solve this by falling back to the default compaction habit. The instruction is specific: finish the current atomic step, write a handoff, clear, resume.
The hook also exits successfully on any internal error. A context monitor should never break the coding session.
The /handoff skill
The user-facing command is a Claude skill called handoff.
Its job is to write one file: HANDOFF.md.
The skill front matter keeps the tool surface tight:
---
name: handoff
description: Write HANDOFF.md for a context rollover so the user can /clear and continue in a fresh session. Use when the context-rollover warning fires, when context usage is high, or when the user asks to hand off / roll over / continue in a new session.
argument-hint: "[optional extra notes to carry over]"
allowed-tools: Read, Write, Bash(git status:*), Bash(git branch:*), Bash(git log:*), Bash(git diff:*), Bash(date:*)
---
Then it captures live repository state at invocation time:
Current repo state (captured at invocation):
- Date: !`date '+%Y-%m-%d %H:%M'`
- Branch: !`git branch --show-current`
- Working tree: !`git status --short`
- Diff summary: !`git diff --stat HEAD`
- Recent commits: !`git log --oneline -5`
That gives the assistant hard facts before it writes the handoff.
The rest of the skill is mostly constraints:
- follow the template exactly
- use real paths and commands
- keep it dense
- include failed attempts
- make the next step immediately executable
- do not include secrets
- overwrite any existing
HANDOFF.md - do not commit anything
The “failed attempts” section is the one I care about most.
Successful work is usually visible in the diff. Failed attempts disappear unless someone writes them down. That is how fresh sessions waste time retrying commands, approaches, or assumptions that already failed twenty minutes ago.
The template
The template is short and intentionally operational:
# Handoff
> Generated: {date} - Branch: {branch} - Previous session: {one-line session label}
## Current Goal
## Current State
## Completed Work
## Key Decisions
## Files Changed
## Failed Attempts / Avoid
## Open Tasks
## Next Concrete Step
## User Preferences / Constraints
This is not a spec.
It does not try to redesign the task. It does not ask for a product brief. It does not split the feature into phases.
It answers one practical question:
If I clear the session right now, what does the next assistant need in order to continue without guessing?
The most important section is Next Concrete Step.
Not “continue the migration.” Not “finish the API.” Not “investigate tests.”
It should be something like:
Open `app/services/billing/reconcile.py` and update `build_reconcile_query`
to include the new `invoice_status` predicate, then rerun
`pytest tests/services/test_billing_reconcile.py`.
That is the difference between a useful handoff and a vague summary.
Resuming after /clear
The resume hook is just as small.
On session start, it looks for HANDOFF.md at the project root:
root = os.environ.get("CLAUDE_PROJECT_DIR") or data.get("cwd") or os.getcwd()
path = os.path.join(root, "HANDOFF.md")
if not os.path.isfile(path):
return
If the file exists, it reads a bounded amount, includes the file age, and injects it as rollover context:
age_h = (time.time() - os.path.getmtime(path)) / 3600
with open(path, encoding="utf-8", errors="replace") as f:
body = f.read(MAX_BYTES)
ctx = (
f"[context-rollover] HANDOFF.md found at the project root "
f"(last modified {age_h:.1f}h ago). It is the handoff from the previous "
"session - resume from it. Read 'Next Concrete Step' and continue there. "
"Briefly confirm to the user that you resumed from HANDOFF.md and state "
"the next step. If the user's first message is unrelated to this handoff, "
"or it looks stale, say so and ask whether to delete HANDOFF.md.\n\n"
"----- HANDOFF.md -----\n" + body
)
That stale-file behavior is important.
HANDOFF.md is useful when it is current. It is dangerous when it silently becomes old context. The hook does not delete or ignore old files by itself; it exposes the age and tells the fresh session to call out a stale or unrelated handoff instead of continuing blindly.
The actual daily flow
The day-to-day usage is simple.
When the context warning appears, I do not immediately clear the session. First I let the assistant finish the current atomic step so the working tree is not left in a broken state.
Then I run:
/handoff
That writes HANDOFF.md. If the task is important, I quickly scan the file, especially Failed Attempts / Avoid and Next Concrete Step.
Then I run:
/clear
In the fresh session, the SessionStart hook loads HANDOFF.md. I usually still send a small prompt such as:
Continue from HANDOFF.md.
That prompt is not doing the loading. The hook already did that. It is just an explicit nudge that the fresh session should continue the rollover instead of treating the first turn as a new unrelated task.
Why this is separate from handoff
My handoff CLI and this Claude setup solve related but different problems.
The CLI is for structured work. It is useful when I want the assistant to move from feature intent to spec, design, task state, decisions, evidence, and drift audit.
This setup is for daily continuity.
It does not ask me to work spec-first. It does not impose a planning phase. It only creates a controlled boundary between one session and the next.
That makes it much easier to use during normal work.
When the task is large or ambiguous, I still prefer the full handoff workflow.
When I am already coding and the context window is getting high, this lighter /handoff is enough.
Porting the idea to Codex or another agent
The exact files will change by tool, but the design ports cleanly.
You need four surfaces:
- persistent instructions
- a reusable handoff command
- lifecycle hooks, if the tool supports them
- a project-local handoff file
For Claude Code, that became:
.claude/settings.json
.claude/skills/handoff/SKILL.md
.claude/templates/HANDOFF.template.md
.claude/scripts/*.py
HANDOFF.md
For Codex, I would map the same idea differently.
Use AGENTS.md for durable repository rules: when to roll over, what HANDOFF.md means, and how fresh sessions should treat it.
Use a Codex skill for the reusable workflow. Codex skills are plain directories with a SKILL.md, and repo-scoped skills can live under .agents/skills. A handoff skill there can carry the same template discipline: inspect git state, write HANDOFF.md, preserve failed attempts, and end with a concrete next step.
Use Codex hooks for lifecycle glue. Codex supports hook events such as UserPromptSubmit, PostToolUse, SessionStart, PreCompact, and PostCompact, configured through hooks.json or config.toml under the relevant .codex layer. That is enough to recreate the same pattern: warn before context rollover, discourage blind compaction, and load HANDOFF.md when a fresh session starts.
The shape would look like this:
AGENTS.md
.agents/
skills/
handoff/
SKILL.md
.codex/
hooks.json
hooks/
context_monitor.py
load_handoff.py
templates/
HANDOFF.template.md
HANDOFF.md
The exact hook payloads and statusline behavior are tool-specific, so I would not blindly copy the Claude scripts into Codex. I would keep the protocol and adapt the integration points.
For tools without hooks, the fallback is still useful:
Ask the assistant to write HANDOFF.md using the template.
Start a new session.
Paste or attach HANDOFF.md.
Tell the assistant to continue from Next Concrete Step.
That is less automatic, but it keeps the most important property: the context boundary is explicit and reviewable.
Full example files
I did not want to turn this post into a wall of Python, so I kept the full example files separate:
These are meant as a starting point, not a package. The important part is the protocol: warn before rollover, write the handoff explicitly, clear the session, and load the handoff into the next one.
The rule I care about
The real rule is simple:
Do not let an invisible summary become the only bridge between two coding sessions.
If the work matters, the bridge should be a file.
It should say what changed, what failed, what decisions were made, what remains open, and what the next concrete step is.
That does not need to be heavyweight.
For big features, I still like structured specs and state files.
For daily coding, a disciplined HANDOFF.md is often enough.