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.

4 min read
lifecycle hooks SDK session start hook agent memory injection settings.json hooks settingSources project

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.json in the project directory where the SDK is running?

sessionstart vs userprompt_submit

Two lifecycle hooks are relevant for memory injection:

HookWhen it fires
session_startOnce at the beginning of a new session
userpromptsubmitEvery 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

Keep Going

Ready to Start Building?

Pick the next step that matches where you are right now.

Tutorial
Claude Code Basics

Start with the terminal basics. A hands-on, step-by-step guide to your first 10 minutes with Claude Code.

Start the Tutorial
Guide
AI-Powered Workflows

Automate your client work. Learn how to connect AI tools into workflows that handle repetitive tasks for you.

Read the Guide
Community
Join the Community

Connect with other fractional leaders building with AI. Share workflows, get feedback, and learn from operators who are ahead of you.

Apply to Join