Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ Detailed API reference for each session hook.
* [Pre-Tool Use](./hooks/pre-tool-use.md): approve, deny, or modify tool calls
* [Post-Tool Use](./hooks/post-tool-use.md): transform tool results
* [User Prompt Submitted](./hooks/user-prompt-submitted.md): modify or filter user messages
* [User Prompt Transformed](./hooks/user-prompt-transformed.md): inspect or replace model-facing prompts
* [Session Lifecycle](./hooks/session-lifecycle.md): session start and end
* [Error Handling](./hooks/error-handling.md): custom error handling

Expand Down
19 changes: 11 additions & 8 deletions docs/features/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,22 @@ A hook is a callback you register once when creating a session. The SDK invokes
```mermaid
flowchart LR
A[Session starts] -->|onSessionStart| B[User sends prompt]
B -->|onUserPromptSubmitted| C[Agent picks a tool]
C -->|onPreToolUse| D[Tool executes]
D -->|onPostToolUse| E{More work?}
E -->|yes| C
E -->|no| F[Session ends]
F -->|onSessionEnd| G((Done))
C -.->|error| H[onErrorOccurred]
D -.->|error| H
B -->|onUserPromptSubmitted| C[Runtime transforms prompt]
C -->|onUserPromptTransformed| D[Agent picks a tool]
D -->|onPreToolUse| E[Tool executes]
E -->|onPostToolUse| F{More work?}
F -->|yes| D
F -->|no| G[Session ends]
G -->|onSessionEnd| H((Done))
D -.->|error| I[onErrorOccurred]
E -.->|error| I
```

| Hook | When it fires | What you can do |
| ------------------------------------------------------------------- | ----------------------------------- | ------------------------------------------ |
| [`onSessionStart`](../hooks/session-lifecycle.md#session-start) | Session begins (new or resumed) | Inject context, load preferences |
| [`onUserPromptSubmitted`](../hooks/user-prompt-submitted.md) | User sends a message | Rewrite prompts, add context, filter input |
| [`onUserPromptTransformed`](../hooks/user-prompt-transformed.md) | Runtime builds the model prompt | Inspect or replace model-facing content |
| [`onPreToolUse`](../hooks/pre-tool-use.md) | Before a tool executes | Allow / deny / modify the call |
| [`onPostToolUse`](../hooks/post-tool-use.md) | After a tool returns (success only) | Transform results, redact secrets, audit |
| [`onPostToolUseFailure`](../hooks/post-tool-use.md#failure-variant) | After a tool returns a failure | Inject retry guidance, log failures |
Expand Down Expand Up @@ -1055,6 +1057,7 @@ For full type definitions, input/output field tables, and additional examples fo
* [Pre-Tool Use](../hooks/pre-tool-use.md)
* [Post-Tool Use](../hooks/post-tool-use.md)
* [User Prompt Submitted](../hooks/user-prompt-submitted.md)
* [User Prompt Transformed](../hooks/user-prompt-transformed.md)
* [Session Lifecycle](../hooks/session-lifecycle.md)
* [Error Handling](../hooks/error-handling.md)

Expand Down
1 change: 1 addition & 0 deletions docs/hooks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ Detailed API reference for each session hook in the GitHub Copilot SDK.
* [Pre-tool use](./pre-tool-use.md): approve, deny, or modify tool calls
* [Post-tool use](./post-tool-use.md): transform tool results
* [User prompt submitted](./user-prompt-submitted.md): modify or filter user messages
* [User prompt transformed](./user-prompt-transformed.md): inspect or replace model-facing prompts
* [Session lifecycle](./session-lifecycle.md): session start and end
* [Error handling](./error-handling.md): custom error handling
2 changes: 2 additions & 0 deletions docs/hooks/hooks-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Hooks allow you to intercept and customize the behavior of Copilot sessions at k
| [`onPostToolUse`](./post-tool-use.md) | After a tool executes (success only) | Result transformation, logging |
| [`onPostToolUseFailure`](./post-tool-use.md#failure-variant) | After a tool execution whose result was a failure | Inject retry guidance, log failures |
| [`onUserPromptSubmitted`](./user-prompt-submitted.md) | When user sends a message | Prompt modification, filtering |
| [`onUserPromptTransformed`](./user-prompt-transformed.md) | After runtime prompt transformation | Inspect or replace model-facing content |
| [`onSessionStart`](./session-lifecycle.md#session-start) | Session begins | Add context, configure session |
| [`onSessionEnd`](./session-lifecycle.md#session-end) | Session ends | Cleanup, analytics |
| [`onErrorOccurred`](./error-handling.md) | Error happens | Custom error handling |
Expand Down Expand Up @@ -263,6 +264,7 @@ const session = await client.createSession({
* **[Pre-Tool Use Hook](./pre-tool-use.md)** - Control tool execution permissions
* **[Post-Tool Use Hook](./post-tool-use.md)** - Transform tool results
* **[User Prompt Submitted Hook](./user-prompt-submitted.md)** - Modify user prompts
* **[User Prompt Transformed Hook](./user-prompt-transformed.md)** - Replace model-facing prompts
* **[Session Lifecycle Hooks](./session-lifecycle.md)** - Session start and end
* **[Agent Stop Hook](./session-lifecycle.md#agent-stop)** - Validate completion before the agent stops
* **[Error Handling Hook](./error-handling.md)** - Custom error handling
Expand Down
129 changes: 129 additions & 0 deletions docs/hooks/user-prompt-transformed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# User prompt transformed hook

The `userPromptTransformed` hook runs after the runtime adds generated context to a submitted prompt, but before the resulting content is persisted to session history or sent to the model.

Use it when you need to inspect or replace the exact model-facing prompt. The `prompt` input contains the user prompt after any `userPromptSubmitted` hooks have run, while `transformedPrompt` also contains runtime-generated context such as `<current_datetime>`.

## Input and output

| Input field | Type | Description |
| --- | --- | --- |
| `sessionId` | string | Runtime session ID |
| `timestamp` | date/time | Time the hook was invoked |
| `cwd` / `workingDirectory` | string | Current working directory |
| `prompt` | string | Prompt after `userPromptSubmitted` hooks |
| `transformedPrompt` | string | Model-facing prompt after runtime transformations |

Return no value to leave the transformed prompt unchanged. Return `modifiedTransformedPrompt` to replace the content that is stored in session history and sent to the model.

## Examples

<details open>
<summary><strong>TypeScript</strong></summary>

<!-- docs-validate: skip -->
```typescript
const session = await client.createSession({
hooks: {
onUserPromptTransformed: async (input) => ({
modifiedTransformedPrompt: redact(input.transformedPrompt),
}),
},
});
```

</details>

<details>
<summary><strong>Python</strong></summary>

<!-- docs-validate: skip -->
```python
session = await client.create_session(
hooks={
"on_user_prompt_transformed": lambda input_data, invocation: {
"modifiedTransformedPrompt": redact(input_data["transformedPrompt"])
}
}
)
```

</details>

<details>
<summary><strong>Go</strong></summary>

<!-- docs-validate: skip -->
```go
session, err := client.CreateSession(ctx, &copilot.SessionConfig{
Hooks: &copilot.SessionHooks{
OnUserPromptTransformed: func(input copilot.UserPromptTransformedHookInput, invocation copilot.HookInvocation) (*copilot.UserPromptTransformedHookOutput, error) {
return &copilot.UserPromptTransformedHookOutput{
ModifiedTransformedPrompt: copilot.String(redact(input.TransformedPrompt)),
}, nil
},
},
})
```

</details>

<details>
<summary><strong>.NET</strong></summary>

<!-- docs-validate: skip -->
```csharp
var session = await client.CreateSessionAsync(new SessionConfig
{
Hooks = new SessionHooks
{
OnUserPromptTransformed = (input, invocation) =>
Task.FromResult<UserPromptTransformedHookOutput?>(new()
{
ModifiedTransformedPrompt = Redact(input.TransformedPrompt),
}),
},
});
```

</details>

<details>
<summary><strong>Java</strong></summary>

<!-- docs-validate: skip -->
```java
var hooks = new SessionHooks().setOnUserPromptTransformed((input, invocation) ->
CompletableFuture.completedFuture(
new UserPromptTransformedHookOutput(redact(input.transformedPrompt()))));

var session = client.createSession(new SessionConfig().setHooks(hooks)).get();
```

</details>

<details>
<summary><strong>Rust</strong></summary>

```rust
#[async_trait]
impl SessionHooks for MyHooks {
async fn on_user_prompt_transformed(
&self,
input: UserPromptTransformedInput,
_ctx: HookContext,
) -> Option<UserPromptTransformedOutput> {
Some(UserPromptTransformedOutput {
modified_transformed_prompt: Some(redact(&input.transformed_prompt)),
})
}
}

let session = client
.create_session(SessionConfig::default().with_hooks(Arc::new(MyHooks)))
.await?;
```

</details>

The replacement is persisted as the user message content, so resumed sessions replay the modified content unchanged.
2 changes: 2 additions & 0 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1097,6 +1097,7 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
config.Hooks.OnPostToolUse != null ||
config.Hooks.OnPostToolUseFailure != null ||
config.Hooks.OnUserPromptSubmitted != null ||
config.Hooks.OnUserPromptTransformed != null ||
config.Hooks.OnSessionStart != null ||
config.Hooks.OnSessionEnd != null ||
config.Hooks.OnErrorOccurred != null ||
Expand Down Expand Up @@ -1327,6 +1328,7 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
config.Hooks.OnPostToolUse != null ||
config.Hooks.OnPostToolUseFailure != null ||
config.Hooks.OnUserPromptSubmitted != null ||
config.Hooks.OnUserPromptTransformed != null ||
config.Hooks.OnSessionStart != null ||
config.Hooks.OnSessionEnd != null ||
config.Hooks.OnErrorOccurred != null ||
Expand Down
7 changes: 7 additions & 0 deletions dotnet/src/Session.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1613,6 +1613,11 @@ internal void RegisterHooks(SessionHooks hooks)
JsonSerializer.Deserialize(input.GetRawText(), SessionJsonContext.Default.UserPromptSubmittedHookInput)!,
invocation)
: null,
"userPromptTransformed" => hooks.OnUserPromptTransformed != null
? await hooks.OnUserPromptTransformed(
JsonSerializer.Deserialize(input.GetRawText(), SessionJsonContext.Default.UserPromptTransformedHookInput)!,
invocation)
: null,
"sessionStart" => hooks.OnSessionStart != null
? await hooks.OnSessionStart(
JsonSerializer.Deserialize(input.GetRawText(), SessionJsonContext.Default.SessionStartHookInput)!,
Expand Down Expand Up @@ -2033,5 +2038,7 @@ internal void ThrowIfDisposed()
[JsonSerializable(typeof(Attachment))]
[JsonSerializable(typeof(UserPromptSubmittedHookInput))]
[JsonSerializable(typeof(UserPromptSubmittedHookOutput))]
[JsonSerializable(typeof(UserPromptTransformedHookInput))]
[JsonSerializable(typeof(UserPromptTransformedHookOutput))]
internal partial class SessionJsonContext : JsonSerializerContext;
}
54 changes: 54 additions & 0 deletions dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1656,6 +1656,55 @@ public sealed class UserPromptSubmittedHookOutput
public bool? SuppressOutput { get; set; }
}

/// <summary>
/// Input for a user-prompt-transformed hook.
/// </summary>
public sealed class UserPromptTransformedHookInput
{
/// <summary>
/// The runtime session ID of the session that triggered the hook.
/// </summary>
[JsonPropertyName("sessionId")]
public string SessionId { get; set; } = string.Empty;

/// <summary>
/// Unix timestamp in milliseconds when the prompt was transformed.
/// </summary>
[JsonPropertyName("timestamp")]
[JsonConverter(typeof(UnixMillisecondsDateTimeOffsetConverter))]
public DateTimeOffset Timestamp { get; set; }

/// <summary>
/// Current working directory of the session.
/// </summary>
[JsonPropertyName("cwd")]
public string WorkingDirectory { get; set; } = string.Empty;

/// <summary>
/// The user prompt after any user-prompt-submitted hooks have run.
/// </summary>
[JsonPropertyName("prompt")]
public string Prompt { get; set; } = string.Empty;

/// <summary>
/// The model-facing prompt after runtime transformations.
/// </summary>
[JsonPropertyName("transformedPrompt")]
public string TransformedPrompt { get; set; } = string.Empty;
}

/// <summary>
/// Output for a user-prompt-transformed hook.
/// </summary>
public sealed class UserPromptTransformedHookOutput
{
/// <summary>
/// Replacement model-facing prompt to persist and send to the model.
/// </summary>
[JsonPropertyName("modifiedTransformedPrompt")]
public string? ModifiedTransformedPrompt { get; set; }
}

/// <summary>
/// Input for a session-start hook.
/// </summary>
Expand Down Expand Up @@ -1967,6 +2016,11 @@ public sealed class SessionHooks
/// </summary>
public Func<UserPromptSubmittedHookInput, HookInvocation, Task<UserPromptSubmittedHookOutput?>>? OnUserPromptSubmitted { get; set; }

/// <summary>
/// Handler called after the runtime transforms a submitted prompt and before it is stored.
/// </summary>
public Func<UserPromptTransformedHookInput, HookInvocation, Task<UserPromptTransformedHookOutput?>>? OnUserPromptTransformed { get; set; }

/// <summary>
/// Handler called when a session starts.
/// </summary>
Expand Down
36 changes: 34 additions & 2 deletions dotnet/test/E2E/HookLifecycleAndOutputE2ETests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ namespace GitHub.Copilot.Test.E2E;
/// <summary>
/// E2E coverage for every handler exposed on <see cref="SessionHooks"/>:
/// OnPreToolUse, OnPostToolUse, OnPostToolUseFailure, OnUserPromptSubmitted,
/// OnSessionStart, OnSessionEnd, OnErrorOccurred, OnAgentStop. Output-shape behavior
/// (modifiedPrompt / additionalContext / errorHandling / modifiedArgs /
/// OnUserPromptTransformed, OnSessionStart, OnSessionEnd, OnErrorOccurred,
/// OnAgentStop. Output-shape behavior (modifiedPrompt / modifiedTransformedPrompt /
/// additionalContext / errorHandling / modifiedArgs /
/// modifiedResult / sessionSummary) is asserted alongside hook invocation. If a
/// new handler is added to <c>SessionHooks</c>, add a corresponding test here.
/// </summary>
Expand Down Expand Up @@ -163,6 +164,37 @@ public async Task Should_Invoke_UserPromptSubmitted_Hook_And_Modify_Prompt()
Assert.Contains("HOOKED_PROMPT", response?.Data.Content ?? string.Empty);
}

[Fact]
public async Task Should_Invoke_UserPromptTransformed_Hook_And_Modify_Transformed_Prompt()
{
var inputs = new List<UserPromptTransformedHookInput>();
var session = await CreateSessionAsync(new SessionConfig
{
Hooks = new SessionHooks
{
OnUserPromptTransformed = (input, invocation) =>
{
inputs.Add(input);
Assert.False(string.IsNullOrWhiteSpace(invocation.SessionId));
return Task.FromResult<UserPromptTransformedHookOutput?>(new UserPromptTransformedHookOutput
{
ModifiedTransformedPrompt = "Reply with exactly: HOOKED_TRANSFORMED_PROMPT",
});
},
},
});

var response = await session.SendAndWaitAsync(new MessageOptions { Prompt = "Answer the request above." });

Assert.NotEmpty(inputs);
Assert.Contains("Answer the request above.", inputs[0].Prompt);
Assert.Contains("Answer the request above.", inputs[0].TransformedPrompt);
Assert.Contains("<current_datetime>", inputs[0].TransformedPrompt);
Assert.True(inputs[0].Timestamp > DateTimeOffset.UnixEpoch);
Assert.False(string.IsNullOrEmpty(inputs[0].WorkingDirectory));
Assert.Contains("HOOKED_TRANSFORMED_PROMPT", response?.Data.Content ?? string.Empty);
}

[Fact]
public async Task Should_Invoke_SessionStart_Hook()
{
Expand Down
2 changes: 2 additions & 0 deletions go/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -871,6 +871,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses
config.Hooks.OnPostToolUse != nil ||
config.Hooks.OnPostToolUseFailure != nil ||
config.Hooks.OnUserPromptSubmitted != nil ||
config.Hooks.OnUserPromptTransformed != nil ||
config.Hooks.OnSessionStart != nil ||
config.Hooks.OnSessionEnd != nil ||
config.Hooks.OnErrorOccurred != nil ||
Expand Down Expand Up @@ -1159,6 +1160,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
config.Hooks.OnPostToolUse != nil ||
config.Hooks.OnPostToolUseFailure != nil ||
config.Hooks.OnUserPromptSubmitted != nil ||
config.Hooks.OnUserPromptTransformed != nil ||
config.Hooks.OnSessionStart != nil ||
config.Hooks.OnSessionEnd != nil ||
config.Hooks.OnErrorOccurred != nil ||
Expand Down
Loading
Loading