Implementing Prompt Enrichment in Copilot SDK: onSessionStart and onUserPromptSubmitted Hooks
The Copilot SDK provides a dual-hook architecture that lets developers enrich prompts at session initialization via onSessionStart and intercept every user message via onUserPromptSubmitted, returning optional initialPrompt or modifiedPrompt strings that the SDK injects before model dispatch.
The GitHub Copilot SDK exposes a lifecycle hook system that enables precise control over prompt flow without modifying core client code. By implementing onSessionStart and onUserPromptSubmitted handlers, you can automatically prepend system instructions, inject repository context, or sanitize user input before it reaches the language model. This guide examines the TypeScript implementation in github/copilot-sdk to show exactly how these hooks modify payload flow.
Hook Dispatcher Architecture
Both hooks route through a single internal dispatcher in Session._handleHooksInvoke (source lines 1881‑1883). The dispatcher first normalizes the incoming wire payload via deserializeHookInput (lines 1869‑1870), converting raw timestamps into native Date objects before invoking your handler.
// Inside Session._handleHooksInvoke
const normalized = deserializeHookInput(input);
const handler = handlerMap[hookType];
if (handler) {
const result = await handler(normalized, { sessionId: this.sessionId });
return result;
}
The handlerMap lookup determines whether the SDK invokes the sessionStart or userPromptSubmitted logic based on the current lifecycle phase.
Enriching Prompts at Session Creation with onSessionStart
The onSessionStart hook fires immediately after a new session is created—before any user messages are processed. It receives a SessionStartHookInput payload containing metadata like timestamp, workingDirectory, and source (indicating whether the session is "new" or "resume").
To inject an initial prompt, return an object with the optional initialPrompt property. The SDK automatically dispatches this string as the first user message, eliminating the need to call session.send immediately after createSession.
const client = new CopilotClient({ /* …credentials… */ });
await client.createSession({
hooks: {
onSessionStart: async (input) => ({
initialPrompt: `You are an expert React developer. Respond concisely.\nWorking directory: ${input.workingDirectory}`
})
}
});
This pattern is ideal for system instruction injection or establishing persistent context that applies to every interaction in the session.
Modifying User Input in Real-Time with onUserPromptSubmitted
Every call to session.send triggers the onUserPromptSubmitted hook via the same dispatcher. The handler receives a UserPromptSubmittedHookInput containing the raw prompt, timestamp, and workingDirectory. Return an object with modifiedPrompt to replace the user’s text before it reaches the model.
const session = await client.createSession({
hooks: {
onUserPromptSubmitted: async (input) => ({
modifiedPrompt: `${input.prompt.trim()}\n\nRespond ONLY with valid JSON.`
})
}
});
await session.send({ prompt: "List the top three files in the repo." });
Use this hook for dynamic enrichment such as appending recent git diffs, enforcing output formats, or masking sensitive literals.
Error Handling and Resilience
The SDK implements defensive fault isolation for hook failures. If your handler throws an exception, the dispatcher catches it at lines 1894‑1898, logs the error, and returns undefined, allowing the session to continue unaffected. This ensures that enrichment logic never crashes the underlying Copilot conversation.
Complete Implementation Example
The following example combines both hooks to establish a static system persona at startup and append dynamic project context to every subsequent user message:
import { CopilotClient } from "@github/copilot-sdk";
async function main() {
const client = new CopilotClient({
// …credentials and config…
});
const session = await client.createSession({
hooks: {
// 1️⃣ Session-start: inject static system instructions
onSessionStart: async (input) => ({
initialPrompt:
"You are a helpful assistant. Answer in plain English.\n" +
`Working directory: ${input.workingDirectory}`
}),
// 2️⃣ User-prompt-submitted: prepend dynamic context
onUserPromptSubmitted: async (input) => {
const projectSummary = await fetchProjectSummary();
return {
modifiedPrompt: `${projectSummary}\n---\n${input.prompt}`
};
},
},
});
// Hooks execute automatically on send()
const reply = await session.send({ prompt: "Explain the core data flow." });
console.log(reply);
}
async function fetchProjectSummary(): Promise<string> {
return "Project: Copilot SDK – multi-language AI extension library.";
}
main().catch(console.error);
Key Source Files and References
| File | Purpose | Link |
|---|---|---|
nodejs/src/session.ts |
Core Session class; contains _handleHooksInvoke dispatcher and hook routing logic. |
session.ts |
nodejs/src/types.ts |
Defines SessionStartHookInput, UserPromptSubmittedHookInput, and return types. |
types.ts |
docs/hooks/session-lifecycle.md |
Documentation for sessionStart hook fields and initialization patterns. |
session-lifecycle.md |
docs/hooks/user-prompt-submitted.md |
Documentation for userPromptSubmitted payload structure and modification rules. |
user-prompt-submitted.md |
nodejs/test/e2e/hooks_extended.e2e.test.ts |
End-to-end tests verifying hook execution and prompt mutation. | hooks_extended.e2e.test.ts |
Summary
onSessionStartfires once duringcreateSession, accepting aninitialPromptthat becomes the first message in the conversation.onUserPromptSubmittedfires on everysession.send, accepting amodifiedPromptthat replaces the raw user input before model dispatch.- Both hooks normalize input via
deserializeHookInputand route throughSession._handleHooksInvokeat lines 1881‑1883. - Error handling at lines 1894‑1898 ensures session continuity even if enrichment logic fails.
- Implementation requires importing from
@github/copilot-sdkand providing an async handler in thehooksconfiguration object.
Frequently Asked Questions
What happens if a hook handler throws an error?
If either onSessionStart or onUserPromptSubmitted throws, the SDK catches the exception at lines 1894‑1898 in session.ts, logs the failure, and treats the hook as returning undefined. The session continues with the original prompt flow unchanged, ensuring that enrichment bugs cannot crash the Copilot client.
Can I use both hooks simultaneously in the same session?
Yes. You can register both onSessionStart and onUserPromptSubmitted in the same hooks object passed to createSession. They operate independently: onSessionStart runs once at initialization, while onUserPromptSubmitted runs before every subsequent session.send call.
Does modifiedPrompt completely replace the original user message?
Yes. When you return a modifiedPrompt string from onUserPromptSubmitted, the SDK substitutes it entirely for the original prompt value before sending the request to the model. To preserve the original text, explicitly include input.prompt in your return value, as shown in the concatenation examples above.
Are these hooks available in all Copilot SDK language bindings?
The hook signatures and payload structures are identical across all official SDK language bindings. While this guide focuses on the TypeScript implementation in github/copilot-sdk, the same onSessionStart and onUserPromptSubmitted patterns exist in Python and other supported runtimes, routing through the equivalent dispatcher logic in each language’s session manager.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →