Lifecycle Hooks in the Claude Agent SDK
Hook into session start, user prompt submit, and sub-agent stop events. Build a memory injection system that loads context automatically at the start of every session.
Why lifecycle hooks exist
Tool hooks intercept individual tool calls. Lifecycle hooks intercept the agent's execution flow itself.
The difference matters. A lifecycle hook fires at structural moments — when a session starts, when a user submits a message, when a sub-agent stops. These are the moments where you want to inject context, load memory, or trigger side effects that have nothing to do with any specific tool.
The practical use case for operators: memory injection at session start. Every agent you build will eventually need context that persists between conversations. Lifecycle hooks give you a clean mechanism to load that context automatically, before the user's first message even processes.
The two ways to configure hooks
The Agent SDK supports two approaches to hooks:
1. Options object — pass the hook directly to the query function. This is what tool hooks use. 2. File-based via settings.json — configure the hook in a .claude/settings.json file and point it at a command or script.
For lifecycle hooks like session_start, you currently need the file-based approach. Some lifecycle hooks have not been fully ported to the TypeScript options API yet. This is a known limitation of the SDK. Check the official hooks reference to confirm which method each hook requires.
> Practical note: If you try to configure a session_start hook in the options object and it silently does nothing, this is why. The file-based system is not a workaround — it is the correct path for that specific hook type.
Building a memory injection hook
Here is how to wire up a session_start hook that automatically loads a memory file into every new session.
Step 1: Create the memory file
Put your persistent context in a memory.md file in your project directory. This might be client preferences, user information, ongoing project context — whatever your agent needs to start each session informed.
Step 2: Create the load-memory script
Create a .claude/load-memory.ts file:
import { readFileSync } from "fs";
import { join } from "path";
const memoryPath = join(process.cwd(), "memory.md");
const content = readFileSync(memoryPath, "utf-8");
// Output in the hook response format
console.log(JSON.stringify({
type: "session_start",
additionalContext: content,
}));
This script reads the memory file and outputs it in the format the hook system expects.
Step 3: Create the settings.json
Create a .claude/settings.json file:
{
"hooks": {
"session_start": {
"type": "command",
"command": "bun .claude/load-memory.ts"
}
}
}
Step 4: Set settingSources in your query call
for await (const message of query(prompt, {
model: "claude-sonnet-4-5",
settingSources: ["project"],
permissionMode: "bypassPermissions",
dangerouslyAllowBypassPermissions: true,
})) {
// handle messages
}
The settingSources: ["project"] line is the piece most people miss. Without it, the SDK ignores your .claude/settings.json entirely. The hook will never fire.
Testing that it works
Send a message that asks what the agent knows about you or about the current project. If the hook is loading correctly, the agent will reference the contents of your memory.md without you including that context in the prompt itself.
If it does not work, check two things first:
- Is
settingSources: ["project"]in your query options? - Is your
.claude/settings.jsonin the project directory where the SDK is running?
sessionstart vs userprompt_submit
Two lifecycle hooks are relevant for memory injection:
| Hook | When it fires |
|---|---|
session_start | Once at the beginning of a new session |
userpromptsubmit | Every time the user sends a message |
For loading a memory file, sessionstart is the right choice. You only need to inject that context once per session. Using userprompt_submit would re-inject on every turn, which adds unnecessary tokens and can create confusing behavior.
Use userpromptsubmit if you need to do something on each user turn — for example, logging messages to an external system or checking rate limits before each query.
> Why this pattern matters for operators: Memory injection is the foundation of any agent that feels like it knows you. When you deploy an agent to a Slack channel or a client-facing tool, the difference between an agent that starts fresh every session and one that remembers context is the difference between a demo and a real workflow. This hook pattern is how you close that gap without building a custom retrieval system.
---
Author: FractionalSkill