Using Claude Code Hooks to Auto-Save Project Context
Claude Code hooks let you run a script at fixed points in a session — before context gets compacted, when a
session ends — so you can call open-context's REST API and save a
summary automatically, without waiting for Claude to remember to call save_context itself.
Wire a PreCompact or SessionEnd hook to POST /api/contexts and every
session leaves a record behind, whether or not anyone asked it to.
Key takeaways
- Claude Code hooks are shell commands (or HTTP calls, MCP tool calls, prompts, or subagents) that fire on lifecycle events like
PreCompact,SessionEnd, andStop. - Each hook receives a JSON payload on stdin, including a
transcript_pathpointing at the full session transcript — enough to build a real summary, not a guess. - A hook can call open-context's REST endpoint directly (
POST /api/contexts) withcurl, with no MCP tool call and no Claude decision involved. PreCompactis the event that matters most for context capture — it fires right before older messages would otherwise be lost to compaction.- Hooks are a safety net, not a replacement for the
save_contextMCP tool — use both, since one is automatic and the other is deliberate.
The gap: memory that depends on Claude remembering to save it
open-context's MCP server gives Claude six tools — save_context, recall_context,
list_contexts, search_contexts, update_context, and
delete_context — that let it write and read memory mid-conversation. That works well when you
explicitly say "remember this." It works less well for everything you didn't think to say out loud: the
three false starts before you landed on an approach, the constraint someone mentioned in passing an hour
ago, the decision that got made and then never repeated. If Claude doesn't call save_context,
none of that survives past the session, no matter how good the tool is.
Hooks close that gap from the other direction. Instead of relying on a tool call Claude has to decide to make, a hook fires deterministically at a point in the session lifecycle and runs regardless of what Claude was thinking about at the time.
What a Claude Code hook actually is
Hooks are configured in settings.json (project or user level) under a hooks key,
one entry per event name. Each entry has a matcher — for tool events like
PreToolUse that matches a tool name, for lifecycle events like PreCompact it's
typically * — and a list of handlers to run. The simplest and most common handler type is
command: a shell script or binary that gets the event's data piped to it as JSON on stdin.
Claude Code fires hooks on dozens of events, but a handful matter for context capture specifically:
| Event | Fires when |
|---|---|
Stop | Claude finishes responding to a single turn |
PreCompact | Right before Claude Code compacts older context out of the window |
SessionEnd | The session terminates |
SessionStart | A session begins or resumes |
Stop fires too often to be useful for a whole-session summary — it runs after every single
turn. PreCompact is the interesting one: it's the exact moment Claude Code is about to discard
detail you might still want, which makes it the natural trigger for "save what matters before it's gone."
Wiring a hook to open-context's REST API
Every hook receives a common JSON payload — session_id, cwd,
hook_event_name, and a transcript_path pointing at the session's JSONL transcript
on disk. A command hook can read that file, pull out the assistant's recent messages, and post them
straight to open-context's HTTP server with nothing more exotic than curl and jq.
The hook script
#!/bin/bash
# .claude/hooks/save-context.sh
INPUT=$(cat)
TRANSCRIPT=$(echo "$INPUT" | jq -r '.transcript_path')
SUMMARY=$(tail -n 60 "$TRANSCRIPT" \
| jq -rs '[.[] | select(.type=="assistant")
| .message.content[]? | select(.type=="text") | .text]
| join("\n\n")' \
| tail -c 4000)
if [ -n "$SUMMARY" ]; then
curl -s -X POST http://localhost:3000/api/contexts \
-H "Content-Type: application/json" \
-d "$(jq -n --arg content "$SUMMARY" \
'{content: $content, tags: ["auto-saved", "hook"], source: "claude-code-hook"}')" \
> /dev/null || true
fi
exit 0
This reads the last 60 lines of the transcript, keeps only the assistant's text messages, trims it to a
reasonable size, and posts it to open-context's POST /api/contexts endpoint — the same
endpoint the web UI and the MCP server's save_context tool both write through. It runs with the
REST API server (npm run server) already up on port 3000; no MCP connection is required for the
save itself.
Registering it in settings.json
{
"hooks": {
"PreCompact": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/save-context.sh",
"timeout": 15
}
]
}
]
}
}
${CLAUDE_PROJECT_DIR} resolves to the project root so the same config works from any
checkout. Add the identical hook under SessionEnd to get one more save whenever a session
closes cleanly, catching context that never triggered a compaction at all.
Hooks vs. asking Claude to call save_context
These are complementary, not competing. Ask Claude directly — "remember that we're pinning Node to 25 for
this repo" — and it calls save_context with a clean, deliberately-written summary and specific
tags. That's the better save whenever you think to make it. A hook is for everything else: the session that
ends without anyone saying "remember this," the long debugging thread where the useful conclusion only
showed up in the last few messages before compaction. One is precise and occasional; the other is coarse
and constant. Run both, and you get deliberate saves for the things you know matter, plus an automatic
backstop for the things you'd only realize mattered later. For the deliberate side of that pairing, see
Giving Claude Code Persistent Project Memory With MCP.
Keeping it safe and quiet
A hook script that fails shouldn't be able to break your session. Command hooks run with a timeout (15
seconds is plenty for a curl call), and for events like PreCompact and
SessionEnd a non-zero exit code doesn't block anything the way it would for
PreToolUse — it just means nothing got saved that round. Ending the script with || true
after the curl call makes that explicit, so a stopped server or a network hiccup never surfaces
as a broken Claude Code session. Nothing here leaves your machine either: the hook talks to
localhost:3000, and everything it saves lands in the same local store
(~/.opencontext/contexts.json by default, or whichever of open-context's 15 supported backends
you've configured) that the MCP tools already use.
Give Claude Code an automatic memory backstop.
Get started with open-context →FAQ
Do I need the MCP server running for a hook to save context, or just the REST API?
Just the REST API. The hook script calls open-context's HTTP endpoint (POST /api/contexts) directly with curl, so the npm run server process needs to be running, but Claude doesn't need to invoke any MCP tool itself for the save to happen.
Which hook event should I use — Stop or PreCompact?
Stop fires after every single turn, which is usually too frequent for a whole-conversation summary. PreCompact fires once, right before Claude Code compacts older context, which is close to the moment detail would otherwise be lost. Pair it with SessionEnd for a final save when a session closes.
Will a hook see the actual conversation content, or do I have to write my own summary?
Hooks receive a transcript_path field pointing at the full session transcript as JSONL, so a hook script can read that file and extract or summarize the parts worth keeping instead of guessing at what happened.
Does this replace the save_context MCP tool?
No. It is a safety net alongside it, not a replacement. Ask Claude to save something mid-conversation with save_context whenever you want it done deliberately, and let a hook catch what falls through the cracks automatically at session boundaries.
Is this safe if the open-context server isn't running?
Yes. A failed curl call just means nothing was saved that round, not a broken Claude Code session — events like PreCompact and SessionEnd don't block on a non-zero hook exit code the way PreToolUse can. Appending || true to the curl call makes that explicit.