Skip to content

Commit 6e748a5

Browse files
Fix Rust hook input deserialization for float timestamps
The Copilot CLI serializes hook input `timestamp` as a JSON float (e.g. `1784203878038.0`). Rust's hand-authored hook input structs typed `timestamp` as `i64`, so `serde_json::from_value` rejected the float, `dispatch_hook` returned an error, and the session handler fell back to an empty `{ "output": {} }` response. Hooks therefore never fired: e.g. a preToolUse deny was dropped, the CLI executed the tool, and the replayed conversation diverged ("No cached response" -> 500). Other SDKs tolerate this incidentally (Go decodes `input` into `any` and re-marshals, dropping the `.0`); Rust decodes strictly. Type the hook input `timestamp` fields as `f64` to match the shape the runtime sends. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ae226b7f-caf9-46f3-b8c5-b9a21c5d7951
1 parent 8062497 commit 6e748a5

3 files changed

Lines changed: 22 additions & 22 deletions

File tree

rust/src/hooks.rs

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ pub struct HookContext {
2727
pub struct PreToolUseInput {
2828
/// The runtime session ID of the session that triggered the hook.
2929
pub session_id: String,
30-
/// Unix timestamp (ms).
31-
pub timestamp: i64,
30+
/// Unix timestamp in ms (the runtime serializes this as a JSON float).
31+
pub timestamp: f64,
3232
/// Working directory.
3333
#[serde(rename = "cwd")]
3434
pub working_directory: PathBuf,
@@ -65,8 +65,8 @@ pub struct PreToolUseOutput {
6565
pub struct PreMcpToolCallInput {
6666
/// The runtime session ID of the session that triggered the hook.
6767
pub session_id: String,
68-
/// Unix timestamp (ms).
69-
pub timestamp: i64,
68+
/// Unix timestamp in ms (the runtime serializes this as a JSON float).
69+
pub timestamp: f64,
7070
/// Working directory.
7171
#[serde(rename = "cwd")]
7272
pub working_directory: PathBuf,
@@ -104,8 +104,8 @@ pub struct PreMcpToolCallOutput {
104104
pub struct PostToolUseInput {
105105
/// The runtime session ID of the session that triggered the hook.
106106
pub session_id: String,
107-
/// Unix timestamp (ms).
108-
pub timestamp: i64,
107+
/// Unix timestamp in ms (the runtime serializes this as a JSON float).
108+
pub timestamp: f64,
109109
/// Working directory.
110110
#[serde(rename = "cwd")]
111111
pub working_directory: PathBuf,
@@ -144,8 +144,8 @@ pub struct PostToolUseOutput {
144144
pub struct PostToolUseFailureInput {
145145
/// The runtime session ID of the session that triggered the hook.
146146
pub session_id: String,
147-
/// Unix timestamp (ms).
148-
pub timestamp: i64,
147+
/// Unix timestamp in ms (the runtime serializes this as a JSON float).
148+
pub timestamp: f64,
149149
/// Working directory.
150150
#[serde(rename = "cwd")]
151151
pub working_directory: PathBuf,
@@ -175,8 +175,8 @@ pub struct PostToolUseFailureOutput {
175175
pub struct UserPromptSubmittedInput {
176176
/// The runtime session ID of the session that triggered the hook.
177177
pub session_id: String,
178-
/// Unix timestamp (ms).
179-
pub timestamp: i64,
178+
/// Unix timestamp in ms (the runtime serializes this as a JSON float).
179+
pub timestamp: f64,
180180
/// Working directory.
181181
#[serde(rename = "cwd")]
182182
pub working_directory: PathBuf,
@@ -205,8 +205,8 @@ pub struct UserPromptSubmittedOutput {
205205
pub struct SessionStartInput {
206206
/// The runtime session ID of the session that triggered the hook.
207207
pub session_id: String,
208-
/// Unix timestamp (ms).
209-
pub timestamp: i64,
208+
/// Unix timestamp in ms (the runtime serializes this as a JSON float).
209+
pub timestamp: f64,
210210
/// Working directory.
211211
#[serde(rename = "cwd")]
212212
pub working_directory: PathBuf,
@@ -235,8 +235,8 @@ pub struct SessionStartOutput {
235235
pub struct SessionEndInput {
236236
/// The runtime session ID of the session that triggered the hook.
237237
pub session_id: String,
238-
/// Unix timestamp (ms).
239-
pub timestamp: i64,
238+
/// Unix timestamp in ms (the runtime serializes this as a JSON float).
239+
pub timestamp: f64,
240240
/// Working directory.
241241
#[serde(rename = "cwd")]
242242
pub working_directory: PathBuf,
@@ -271,8 +271,8 @@ pub struct SessionEndOutput {
271271
pub struct ErrorOccurredInput {
272272
/// The runtime session ID of the session that triggered the hook.
273273
pub session_id: String,
274-
/// Unix timestamp (ms).
275-
pub timestamp: i64,
274+
/// Unix timestamp in ms (the runtime serializes this as a JSON float).
275+
pub timestamp: f64,
276276
/// Working directory.
277277
#[serde(rename = "cwd")]
278278
pub working_directory: PathBuf,

rust/tests/e2e/hooks_extended.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ async fn should_invoke_onsessionstart_hook_on_new_session() {
3636
session.send_and_wait("Say hi").await.expect("send");
3737
let input = recv_with_timeout(&mut rx, "sessionStart hook").await;
3838
assert_eq!(input.source, "new");
39-
assert!(input.timestamp > 0);
39+
assert!(input.timestamp > 0.0);
4040
assert!(!input.working_directory.as_os_str().is_empty());
4141

4242
session.disconnect().await.expect("disconnect session");
@@ -68,7 +68,7 @@ async fn should_invoke_onuserpromptsubmitted_hook_when_sending_a_message() {
6868
session.send_and_wait("Say hello").await.expect("send");
6969
let input = recv_with_timeout(&mut rx, "userPromptSubmitted hook").await;
7070
assert!(input.prompt.contains("Say hello"));
71-
assert!(input.timestamp > 0);
71+
assert!(input.timestamp > 0.0);
7272
assert!(!input.working_directory.as_os_str().is_empty());
7373

7474
session.disconnect().await.expect("disconnect session");
@@ -100,7 +100,7 @@ async fn should_invoke_onsessionend_hook_when_session_is_disconnected() {
100100
session.send_and_wait("Say hi").await.expect("send");
101101
session.disconnect().await.expect("disconnect session");
102102
let input = recv_with_timeout(&mut rx, "sessionEnd hook").await;
103-
assert!(input.timestamp > 0);
103+
assert!(input.timestamp > 0.0);
104104
assert!(!input.working_directory.as_os_str().is_empty());
105105

106106
client.stop().await.expect("stop client");
@@ -237,7 +237,7 @@ async fn should_invoke_sessionend_hook() {
237237
session.send_and_wait("Say bye").await.expect("send");
238238
session.disconnect().await.expect("disconnect session");
239239
let input = recv_with_timeout(&mut rx, "sessionEnd hook").await;
240-
assert!(input.timestamp > 0);
240+
assert!(input.timestamp > 0.0);
241241

242242
client.stop().await.expect("stop client");
243243
})
@@ -412,7 +412,7 @@ async fn should_invoke_posttoolusefailure_hook_for_failed_tool_result() {
412412
.as_str()
413413
.is_some_and(|path| path.contains("missing.txt"))
414414
);
415-
assert!(input.timestamp > 0);
415+
assert!(input.timestamp > 0.0);
416416
assert!(!input.working_directory.as_os_str().is_empty());
417417
assert!(
418418
assistant_message_content(&answer).contains("HOOK_FAILURE_GUIDANCE_APPLIED")

rust/tests/e2e/pre_mcp_tool_call_hook.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ async fn should_set_meta_via_premcptoolcall_hook() {
126126
assert_eq!(input.server_name, "meta-echo");
127127
assert_eq!(input.tool_name, "echo_meta");
128128
assert!(!input.working_directory.as_os_str().is_empty());
129-
assert!(input.timestamp > 0);
129+
assert!(input.timestamp > 0.0);
130130

131131
session.disconnect().await.expect("disconnect session");
132132
client.stop().await.expect("stop client");

0 commit comments

Comments
 (0)