fix: decode windows agent hook stdin as utf-8 (#5270)

Every bundled Windows PowerShell hook read its hook JSON with
`[Console]::In.ReadToEnd()`, which decodes stdin using the process console
input code page instead of UTF-8. On Korean Windows (CP949) that corrupts
the payload, `ConvertFrom-Json` throws, and the hook silently exits without
reporting the session. Read the raw stdin stream as UTF-8 (BOM-aware) in
every PowerShell hook and bump the affected integration versions.

refs #5253

(cherry picked from commit 00584a2794)
This commit is contained in:
JJ Liebig
2026-10-14 18:22:31 +03:00
committed by Ogulcan Celik
parent aada5ac3c0
commit 808790bbc1
28 changed files with 230 additions and 64 deletions
@@ -2,7 +2,7 @@
# managed by herdr; reinstalling or updating the integration overwrites this file.
# add custom hooks beside this file instead of editing it.
# HERDR_INTEGRATION_ID=antigravity_cli
# HERDR_INTEGRATION_VERSION=3
# HERDR_INTEGRATION_VERSION=4
# Session-only: this hook reports the Antigravity conversation so Herdr can
# resume the pane. Lifecycle state comes from Herdr's screen detection.
@@ -20,7 +20,10 @@ if ($Action -ne "session") { Exit-Hook }
if ($env:HERDR_ENV -ne "1") { Exit-Hook }
if ([string]::IsNullOrWhiteSpace($env:HERDR_PANE_ID)) { Exit-Hook }
$inputText = [Console]::In.ReadToEnd()
# Read hook input as raw bytes so UTF-8 payloads decode correctly even when the
# console input code page is not UTF-8 (for example CP949 on Korean Windows).
$stdinReader = New-Object System.IO.StreamReader([Console]::OpenStandardInput(), [System.Text.Encoding]::UTF8)
$inputText = $stdinReader.ReadToEnd()
try {
$payload = if ([string]::IsNullOrWhiteSpace($inputText)) { $null } else { $inputText | ConvertFrom-Json }
} catch {
@@ -3,7 +3,7 @@
# managed by herdr; reinstalling or updating the integration overwrites this file.
# add custom hooks beside this file instead of editing it.
# HERDR_INTEGRATION_ID=antigravity_cli
# HERDR_INTEGRATION_VERSION=3
# HERDR_INTEGRATION_VERSION=4
# Session-only: this hook reports the Antigravity conversation so Herdr can
# resume the pane. Lifecycle state comes from Herdr's screen detection.
@@ -2,7 +2,7 @@
# managed by herdr; reinstalling or updating the integration overwrites this file.
# add custom hooks beside this file instead of editing it.
# HERDR_INTEGRATION_ID=claude
# HERDR_INTEGRATION_VERSION=10
# HERDR_INTEGRATION_VERSION=11
param([string]$Action = "")
@@ -10,7 +10,10 @@ if ($Action -ne "session") { exit 0 }
if ($env:HERDR_ENV -ne "1") { exit 0 }
if ([string]::IsNullOrWhiteSpace($env:HERDR_PANE_ID)) { exit 0 }
$inputText = [Console]::In.ReadToEnd()
# Read hook input as raw bytes so UTF-8 payloads decode correctly even when the
# console input code page is not UTF-8 (for example CP949 on Korean Windows).
$stdinReader = New-Object System.IO.StreamReader([Console]::OpenStandardInput(), [System.Text.Encoding]::UTF8)
$inputText = $stdinReader.ReadToEnd()
try {
$payload = if ([string]::IsNullOrWhiteSpace($inputText)) { $null } else { $inputText | ConvertFrom-Json }
} catch {
@@ -3,7 +3,7 @@
# managed by herdr; reinstalling or updating the integration overwrites this file.
# add custom hooks beside this file instead of editing it.
# HERDR_INTEGRATION_ID=claude
# HERDR_INTEGRATION_VERSION=10
# HERDR_INTEGRATION_VERSION=11
set -eu
@@ -2,7 +2,7 @@
# managed by herdr; reinstalling or updating the integration overwrites this file.
# add custom hooks beside this file instead of editing it.
# HERDR_INTEGRATION_ID=codex
# HERDR_INTEGRATION_VERSION=9
# HERDR_INTEGRATION_VERSION=10
param([string]$Action = "")
@@ -10,7 +10,10 @@ if ($Action -ne "session") { exit 0 }
if ($env:HERDR_ENV -ne "1") { exit 0 }
if ([string]::IsNullOrWhiteSpace($env:HERDR_PANE_ID)) { exit 0 }
$inputText = [Console]::In.ReadToEnd()
# Read hook input as raw bytes so UTF-8 payloads decode correctly even when the
# console input code page is not UTF-8 (for example CP949 on Korean Windows).
$stdinReader = New-Object System.IO.StreamReader([Console]::OpenStandardInput(), [System.Text.Encoding]::UTF8)
$inputText = $stdinReader.ReadToEnd()
try {
$payload = if ([string]::IsNullOrWhiteSpace($inputText)) { $null } else { $inputText | ConvertFrom-Json }
} catch {
@@ -3,7 +3,7 @@
# managed by herdr; reinstalling or updating the integration overwrites this file.
# add custom hooks beside this file instead of editing it.
# HERDR_INTEGRATION_ID=codex
# HERDR_INTEGRATION_VERSION=9
# HERDR_INTEGRATION_VERSION=10
set -eu
@@ -2,13 +2,16 @@
# managed by herdr; reinstalling or updating the integration overwrites this file.
# add custom hooks beside this file instead of editing it.
# HERDR_INTEGRATION_ID=copilot
# HERDR_INTEGRATION_VERSION=3
# HERDR_INTEGRATION_VERSION=4
if ($env:HERDR_ENV -ne "1") { exit 0 }
if ([string]::IsNullOrWhiteSpace($env:HERDR_PANE_ID)) { exit 0 }
if ([string]::IsNullOrWhiteSpace($env:HERDR_SOCKET_PATH)) { exit 0 }
$inputText = [Console]::In.ReadToEnd()
# Read hook input as raw bytes so UTF-8 payloads decode correctly even when the
# console input code page is not UTF-8 (for example CP949 on Korean Windows).
$stdinReader = New-Object System.IO.StreamReader([Console]::OpenStandardInput(), [System.Text.Encoding]::UTF8)
$inputText = $stdinReader.ReadToEnd()
try {
$payload = if ([string]::IsNullOrWhiteSpace($inputText)) { @{} } else { $inputText | ConvertFrom-Json }
} catch {
@@ -3,7 +3,7 @@
# managed by herdr; reinstalling or updating the integration overwrites this file.
# add custom hooks beside this file instead of editing it.
# HERDR_INTEGRATION_ID=copilot
# HERDR_INTEGRATION_VERSION=3
# HERDR_INTEGRATION_VERSION=4
set -eu
@@ -1,6 +1,6 @@
# managed by herdr; reinstalling the integration replaces this file.
# HERDR_INTEGRATION_ID=cursor
# HERDR_INTEGRATION_VERSION=1
# HERDR_INTEGRATION_VERSION=2
param([string]$Action = "")
@@ -13,7 +13,10 @@ if ($Action -ne "session") { Exit-Hook }
if ($env:HERDR_ENV -ne "1") { Exit-Hook }
if ([string]::IsNullOrWhiteSpace($env:HERDR_PANE_ID)) { Exit-Hook }
$inputText = [Console]::In.ReadToEnd()
# Read hook input as raw bytes so UTF-8 payloads decode correctly even when the
# console input code page is not UTF-8 (for example CP949 on Korean Windows).
$stdinReader = New-Object System.IO.StreamReader([Console]::OpenStandardInput(), [System.Text.Encoding]::UTF8)
$inputText = $stdinReader.ReadToEnd()
$jsonStart = $inputText.IndexOf("{")
if ($jsonStart -gt 0) {
$inputText = $inputText.Substring($jsonStart)
@@ -1,7 +1,7 @@
#!/bin/sh
# managed by herdr; reinstalling the integration replaces this file.
# HERDR_INTEGRATION_ID=cursor
# HERDR_INTEGRATION_VERSION=1
# HERDR_INTEGRATION_VERSION=2
[ "${1:-}" = "session" ] || exit 0
[ "${HERDR_ENV:-}" = "1" ] || exit 0
@@ -2,7 +2,7 @@
# managed by herdr; reinstalling or updating the integration overwrites this file.
# add custom hooks beside this file instead of editing it.
# HERDR_INTEGRATION_ID=devin
# HERDR_INTEGRATION_VERSION=2
# HERDR_INTEGRATION_VERSION=3
param([string]$Action = "")
@@ -10,7 +10,10 @@ if ($Action -ne "session") { exit 0 }
if ($env:HERDR_ENV -ne "1") { exit 0 }
if ([string]::IsNullOrWhiteSpace($env:HERDR_PANE_ID)) { exit 0 }
$inputText = [Console]::In.ReadToEnd()
# Read hook input as raw bytes so UTF-8 payloads decode correctly even when the
# console input code page is not UTF-8 (for example CP949 on Korean Windows).
$stdinReader = New-Object System.IO.StreamReader([Console]::OpenStandardInput(), [System.Text.Encoding]::UTF8)
$inputText = $stdinReader.ReadToEnd()
try {
$payload = if ([string]::IsNullOrWhiteSpace($inputText)) { $null } else { $inputText | ConvertFrom-Json }
} catch {
@@ -3,7 +3,7 @@
# managed by herdr; reinstalling or updating the integration overwrites this file.
# add custom hooks beside this file instead of editing it.
# HERDR_INTEGRATION_ID=devin
# HERDR_INTEGRATION_VERSION=2
# HERDR_INTEGRATION_VERSION=3
set -eu
@@ -2,7 +2,7 @@
# managed by herdr; reinstalling or updating the integration overwrites this file.
# add custom hooks beside this file instead of editing it.
# HERDR_INTEGRATION_ID=droid
# HERDR_INTEGRATION_VERSION=3
# HERDR_INTEGRATION_VERSION=4
param([string]$Action = "")
@@ -10,7 +10,10 @@ if ($Action -ne "session") { exit 0 }
if ($env:HERDR_ENV -ne "1") { exit 0 }
if ([string]::IsNullOrWhiteSpace($env:HERDR_PANE_ID)) { exit 0 }
$inputText = [Console]::In.ReadToEnd()
# Read hook input as raw bytes so UTF-8 payloads decode correctly even when the
# console input code page is not UTF-8 (for example CP949 on Korean Windows).
$stdinReader = New-Object System.IO.StreamReader([Console]::OpenStandardInput(), [System.Text.Encoding]::UTF8)
$inputText = $stdinReader.ReadToEnd()
try {
$payload = if ([string]::IsNullOrWhiteSpace($inputText)) { $null } else { $inputText | ConvertFrom-Json }
} catch {
@@ -3,7 +3,7 @@
# managed by herdr; reinstalling or updating the integration overwrites this file.
# add custom hooks beside this file instead of editing it.
# HERDR_INTEGRATION_ID=droid
# HERDR_INTEGRATION_VERSION=3
# HERDR_INTEGRATION_VERSION=4
set -eu
@@ -2,7 +2,7 @@
# managed by herdr; reinstalling or updating the integration overwrites this file.
# add custom hooks beside this file instead of editing it.
# HERDR_INTEGRATION_ID=grok
# HERDR_INTEGRATION_VERSION=2
# HERDR_INTEGRATION_VERSION=3
param([string]$Action = "")
@@ -10,7 +10,10 @@ if ($Action -ne "session") { exit 0 }
if ($env:HERDR_ENV -ne "1") { exit 0 }
if ([string]::IsNullOrWhiteSpace($env:HERDR_PANE_ID)) { exit 0 }
$inputText = [Console]::In.ReadToEnd()
# Read hook input as raw bytes so UTF-8 payloads decode correctly even when the
# console input code page is not UTF-8 (for example CP949 on Korean Windows).
$stdinReader = New-Object System.IO.StreamReader([Console]::OpenStandardInput(), [System.Text.Encoding]::UTF8)
$inputText = $stdinReader.ReadToEnd()
try {
$payload = if ([string]::IsNullOrWhiteSpace($inputText)) { $null } else { $inputText | ConvertFrom-Json }
} catch {
@@ -3,7 +3,7 @@
# managed by herdr; reinstalling or updating the integration overwrites this file.
# add custom hooks beside this file instead of editing it.
# HERDR_INTEGRATION_ID=grok
# HERDR_INTEGRATION_VERSION=2
# HERDR_INTEGRATION_VERSION=3
set -eu
@@ -2,7 +2,7 @@
# managed by herdr; reinstalling or updating the integration overwrites this file.
# add custom hooks beside this file instead of editing it.
# HERDR_INTEGRATION_ID=kimi
# HERDR_INTEGRATION_VERSION=7
# HERDR_INTEGRATION_VERSION=8
param([string]$Action = "")
@@ -10,7 +10,10 @@ if (@("session", "working", "blocked", "idle") -notcontains $Action) { exit 0 }
if ($env:HERDR_ENV -ne "1") { exit 0 }
if ([string]::IsNullOrWhiteSpace($env:HERDR_PANE_ID)) { exit 0 }
$inputText = [Console]::In.ReadToEnd()
# Read hook input as raw bytes so UTF-8 payloads decode correctly even when the
# console input code page is not UTF-8 (for example CP949 on Korean Windows).
$stdinReader = New-Object System.IO.StreamReader([Console]::OpenStandardInput(), [System.Text.Encoding]::UTF8)
$inputText = $stdinReader.ReadToEnd()
try {
$payload = if ([string]::IsNullOrWhiteSpace($inputText)) { $null } else { $inputText | ConvertFrom-Json }
} catch {
@@ -1,7 +1,7 @@
#!/bin/sh
# managed by herdr; reinstalling the integration replaces this file.
# HERDR_INTEGRATION_ID=kimi
# HERDR_INTEGRATION_VERSION=7
# HERDR_INTEGRATION_VERSION=8
action="${1:-}"
case "$action" in
@@ -1,6 +1,6 @@
# managed by herdr; reinstalling the integration replaces this file.
# HERDR_INTEGRATION_ID=letta
# HERDR_INTEGRATION_VERSION=1
# HERDR_INTEGRATION_VERSION=2
param([string]$Action = "")
@@ -9,7 +9,10 @@ if ($env:HERDR_ENV -ne "1") { exit 0 }
if ([string]::IsNullOrWhiteSpace($env:HERDR_PANE_ID)) { exit 0 }
if ([string]::IsNullOrWhiteSpace($env:HERDR_SOCKET_PATH)) { exit 0 }
$inputText = [Console]::In.ReadToEnd()
# Read hook input as raw bytes so UTF-8 payloads decode correctly even when the
# console input code page is not UTF-8 (for example CP949 on Korean Windows).
$stdinReader = New-Object System.IO.StreamReader([Console]::OpenStandardInput(), [System.Text.Encoding]::UTF8)
$inputText = $stdinReader.ReadToEnd()
try {
$payload = if ([string]::IsNullOrWhiteSpace($inputText)) { $null } else { $inputText | ConvertFrom-Json }
} catch {
@@ -1,7 +1,7 @@
#!/bin/sh
# managed by herdr; reinstalling the integration replaces this file.
# HERDR_INTEGRATION_ID=letta
# HERDR_INTEGRATION_VERSION=1
# HERDR_INTEGRATION_VERSION=2
[ "${1:-}" = "session" ] || exit 0
[ "${HERDR_ENV:-}" = "1" ] || exit 0
@@ -2,7 +2,7 @@
# managed by herdr; reinstalling or updating the integration overwrites this file.
# add custom hooks beside this file instead of editing it.
# HERDR_INTEGRATION_ID=mastracode
# HERDR_INTEGRATION_VERSION=2
# HERDR_INTEGRATION_VERSION=3
param([string]$Action = "")
@@ -10,7 +10,10 @@ if ($Action -notin @("session", "working", "idle", "blocked")) { exit 0 }
if ($env:HERDR_ENV -ne "1") { exit 0 }
if ([string]::IsNullOrWhiteSpace($env:HERDR_PANE_ID)) { exit 0 }
$inputText = [Console]::In.ReadToEnd()
# Read hook input as raw bytes so UTF-8 payloads decode correctly even when the
# console input code page is not UTF-8 (for example CP949 on Korean Windows).
$stdinReader = New-Object System.IO.StreamReader([Console]::OpenStandardInput(), [System.Text.Encoding]::UTF8)
$inputText = $stdinReader.ReadToEnd()
try {
$payload = if ([string]::IsNullOrWhiteSpace($inputText)) { $null } else { $inputText | ConvertFrom-Json }
} catch {
@@ -3,7 +3,7 @@
# managed by herdr; reinstalling or updating the integration overwrites this file.
# add custom hooks beside this file instead of editing it.
# HERDR_INTEGRATION_ID=mastracode
# HERDR_INTEGRATION_VERSION=2
# HERDR_INTEGRATION_VERSION=3
set -eu
@@ -2,7 +2,7 @@
# managed by herdr; reinstalling or updating the integration overwrites this file.
# add custom hooks beside this file instead of editing it.
# HERDR_INTEGRATION_ID=qodercli
# HERDR_INTEGRATION_VERSION=3
# HERDR_INTEGRATION_VERSION=4
param([string]$Action = "")
@@ -10,7 +10,10 @@ if ($Action -ne "session") { exit 0 }
if ($env:HERDR_ENV -ne "1") { exit 0 }
if ([string]::IsNullOrWhiteSpace($env:HERDR_PANE_ID)) { exit 0 }
$inputText = [Console]::In.ReadToEnd()
# Read hook input as raw bytes so UTF-8 payloads decode correctly even when the
# console input code page is not UTF-8 (for example CP949 on Korean Windows).
$stdinReader = New-Object System.IO.StreamReader([Console]::OpenStandardInput(), [System.Text.Encoding]::UTF8)
$inputText = $stdinReader.ReadToEnd()
try {
$payload = if ([string]::IsNullOrWhiteSpace($inputText)) { $null } else { $inputText | ConvertFrom-Json }
} catch {
@@ -1,7 +1,7 @@
#!/bin/sh
# managed by herdr; reinstalling the integration replaces this file.
# HERDR_INTEGRATION_ID=qodercli
# HERDR_INTEGRATION_VERSION=3
# HERDR_INTEGRATION_VERSION=4
[ "${1:-}" = "session" ] || exit 0
[ "${HERDR_ENV:-}" = "1" ] || exit 0
@@ -2,7 +2,7 @@
# managed by herdr; reinstalling or updating the integration overwrites this file.
# add custom hooks beside this file instead of editing it.
# HERDR_INTEGRATION_ID=qwen
# HERDR_INTEGRATION_VERSION=1
# HERDR_INTEGRATION_VERSION=2
param([string]$Action = "")
@@ -11,7 +11,10 @@ if ($env:HERDR_ENV -ne "1") { exit 0 }
if ([string]::IsNullOrWhiteSpace($env:HERDR_PANE_ID)) { exit 0 }
if ([string]::IsNullOrWhiteSpace($env:HERDR_SOCKET_PATH)) { exit 0 }
$inputText = [Console]::In.ReadToEnd()
# Read hook input as raw bytes so UTF-8 payloads decode correctly even when the
# console input code page is not UTF-8 (for example CP949 on Korean Windows).
$stdinReader = New-Object System.IO.StreamReader([Console]::OpenStandardInput(), [System.Text.Encoding]::UTF8)
$inputText = $stdinReader.ReadToEnd()
try {
$payload = if ([string]::IsNullOrWhiteSpace($inputText)) { $null } else { $inputText | ConvertFrom-Json }
} catch {
@@ -1,7 +1,7 @@
#!/bin/sh
# managed by herdr; reinstalling the integration replaces this file.
# HERDR_INTEGRATION_ID=qwen
# HERDR_INTEGRATION_VERSION=1
# HERDR_INTEGRATION_VERSION=2
[ "${1:-}" = "session" ] || exit 0
[ "${HERDR_ENV:-}" = "1" ] || exit 0
+13 -13
View File
@@ -49,7 +49,7 @@ const CLAUDE_HOOK_ASSET: &str = if cfg!(windows) {
} else {
include_str!("assets/claude/herdr-agent-state.sh")
};
const CLAUDE_INTEGRATION_VERSION: u32 = 10;
const CLAUDE_INTEGRATION_VERSION: u32 = 11;
const CODEX_HOOK_INSTALL_NAME: &str = if cfg!(windows) {
"herdr-agent-state.ps1"
} else {
@@ -60,7 +60,7 @@ const CODEX_HOOK_ASSET: &str = if cfg!(windows) {
} else {
include_str!("assets/codex/herdr-agent-state.sh")
};
const CODEX_INTEGRATION_VERSION: u32 = 9;
const CODEX_INTEGRATION_VERSION: u32 = 10;
const KIMI_HOOK_INSTALL_NAME: &str = if cfg!(windows) {
"herdr-agent-state.ps1"
} else {
@@ -71,7 +71,7 @@ const KIMI_HOOK_ASSET: &str = if cfg!(windows) {
} else {
include_str!("assets/kimi/herdr-agent-state.sh")
};
const KIMI_INTEGRATION_VERSION: u32 = 7;
const KIMI_INTEGRATION_VERSION: u32 = 8;
const KIMI_CONFIG_BLOCK_BEGIN: &str = "# >>> herdr kimi integration";
const KIMI_CONFIG_BLOCK_END: &str = "# <<< herdr kimi integration";
const KIMI_MIN_VERSION: &str = "0.14.0";
@@ -113,7 +113,7 @@ const COPILOT_HOOK_ASSET: &str = if cfg!(windows) {
} else {
include_str!("assets/copilot/herdr-agent-state.sh")
};
const COPILOT_INTEGRATION_VERSION: u32 = 3;
const COPILOT_INTEGRATION_VERSION: u32 = 4;
const COPILOT_HOOK_EVENTS: [&str; 1] = ["SessionStart"];
const COPILOT_REMOVED_LIFECYCLE_HOOK_EVENTS: [&str; 9] = [
"UserPromptSubmit",
@@ -136,7 +136,7 @@ const DEVIN_HOOK_ASSET: &str = if cfg!(windows) {
} else {
include_str!("assets/devin/herdr-agent-state.sh")
};
const DEVIN_INTEGRATION_VERSION: u32 = 2;
const DEVIN_INTEGRATION_VERSION: u32 = 3;
const DEVIN_HOOK_EVENTS: [(&str, &str); 6] = [
("SessionStart", "session"),
("UserPromptSubmit", "session"),
@@ -163,7 +163,7 @@ const DROID_HOOK_ASSET: &str = if cfg!(windows) {
} else {
include_str!("assets/droid/herdr-agent-state.sh")
};
const DROID_INTEGRATION_VERSION: u32 = 3;
const DROID_INTEGRATION_VERSION: u32 = 4;
const DROID_HOOK_EVENTS: [(&str, &str); 1] = [("SessionStart", "session")];
const DROID_REMOVED_LIFECYCLE_HOOK_EVENTS: [(&str, &str); 9] = [
("SessionStart", "idle"),
@@ -204,7 +204,7 @@ const QODERCLI_HOOK_ASSET: &str = if cfg!(windows) {
} else {
include_str!("assets/qodercli/herdr-agent-state.sh")
};
const QODERCLI_INTEGRATION_VERSION: u32 = 3;
const QODERCLI_INTEGRATION_VERSION: u32 = 4;
const QODERCLI_HOOK_EVENTS: [(&str, &str); 1] = [("SessionStart", "session")];
const QWEN_HOOK_INSTALL_NAME: &str = if cfg!(windows) {
"herdr-agent-session.ps1"
@@ -216,7 +216,7 @@ const QWEN_HOOK_ASSET: &str = if cfg!(windows) {
} else {
include_str!("assets/qwen/herdr-agent-session.sh")
};
const QWEN_INTEGRATION_VERSION: u32 = 1;
const QWEN_INTEGRATION_VERSION: u32 = 2;
const QWEN_HOOK_EVENTS: [(&str, &str); 1] = [("SessionStart", "session")];
const LETTA_HOOK_INSTALL_NAME: &str = if cfg!(windows) {
"herdr-agent-session.ps1"
@@ -228,7 +228,7 @@ const LETTA_HOOK_ASSET: &str = if cfg!(windows) {
} else {
include_str!("assets/letta/herdr-agent-session.sh")
};
const LETTA_INTEGRATION_VERSION: u32 = 1;
const LETTA_INTEGRATION_VERSION: u32 = 2;
const LETTA_HOOK_TIMEOUT_MS: u64 = 10_000;
const QODERCLI_REMOVED_LIFECYCLE_HOOK_EVENTS: [(&str, &str); 12] = [
("SessionStart", "idle"),
@@ -254,7 +254,7 @@ const CURSOR_HOOK_ASSET: &str = if cfg!(windows) {
} else {
include_str!("assets/cursor/herdr-agent-state.sh")
};
const CURSOR_INTEGRATION_VERSION: u32 = 1;
const CURSOR_INTEGRATION_VERSION: u32 = 2;
#[cfg(windows)]
const ANTIGRAVITY_CLI_HOOK_INSTALL_NAME: &str = "herdr-agent-state.ps1";
#[cfg(not(windows))]
@@ -265,7 +265,7 @@ const ANTIGRAVITY_CLI_HOOK_ASSET: &str =
#[cfg(not(windows))]
const ANTIGRAVITY_CLI_HOOK_ASSET: &str =
include_str!("assets/antigravity_cli/herdr-agent-state.sh");
const ANTIGRAVITY_CLI_INTEGRATION_VERSION: u32 = 3;
const ANTIGRAVITY_CLI_INTEGRATION_VERSION: u32 = 4;
/// Antigravity CLI keys `hooks.json` by hook name, so every Herdr entry lives
/// under one Herdr-owned block that install rewrites and uninstall removes.
const ANTIGRAVITY_CLI_HOOK_BLOCK_NAME: &str = "herdr";
@@ -291,7 +291,7 @@ const MASTRACODE_HOOK_ASSET: &str = if cfg!(windows) {
} else {
include_str!("assets/mastracode/herdr-agent-state.sh")
};
const MASTRACODE_INTEGRATION_VERSION: u32 = 2;
const MASTRACODE_INTEGRATION_VERSION: u32 = 3;
const MASTRACODE_HOOK_TIMEOUT_MS: u64 = 10_000;
const MASTRACODE_REMOVED_HOOK_EVENTS: [(&str, &str); 2] =
[("SessionStart", "idle"), ("SessionEnd", "release")];
@@ -319,7 +319,7 @@ const GROK_HOOK_ASSET: &str = if cfg!(windows) {
} else {
include_str!("assets/grok/herdr-agent-state.sh")
};
const GROK_INTEGRATION_VERSION: u32 = 2;
const GROK_INTEGRATION_VERSION: u32 = 3;
pub(crate) const INSTALL_WARNING_PREFIX: &str = "warning:";
+139 -12
View File
@@ -1132,7 +1132,7 @@ fn claude_v9_integration_status_is_outdated_until_reinstalled() {
assert_eq!(claude.path, hook_path);
assert_eq!(claude.installed_version, Some(9));
assert_eq!(claude.expected_version, 10);
assert_eq!(claude.expected_version, 11);
assert_eq!(claude.state, IntegrationStatusKind::Outdated);
install_claude().unwrap();
@@ -1141,7 +1141,7 @@ fn claude_v9_integration_status_is_outdated_until_reinstalled() {
hook_path,
CLAUDE_INTEGRATION_VERSION,
);
assert_eq!(status.installed_version, Some(10));
assert_eq!(status.installed_version, Some(11));
assert_eq!(status.state, IntegrationStatusKind::Current);
std::env::remove_var("HOME");
@@ -1171,13 +1171,139 @@ fn claude_v2_integration_status_is_outdated() {
assert_eq!(claude.path, hook_path);
assert_eq!(claude.installed_version, Some(2));
assert_eq!(claude.expected_version, 10);
assert_eq!(claude.expected_version, 11);
assert_eq!(claude.state, IntegrationStatusKind::Outdated);
std::env::remove_var("HOME");
let _ = fs::remove_dir_all(base);
}
#[test]
fn windows_hooks_decode_stdin_as_utf8() {
// Windows PowerShell decodes `[Console]::In` with the console input code
// page, which corrupts UTF-8 hook payloads on non-UTF-8 locales such as
// Korean Windows (CP949) and makes the hook silently drop the session.
// Every bundled PowerShell hook must read the raw stdin stream as UTF-8.
// See #5253.
for (name, asset) in [
(
"antigravity_cli",
include_str!("assets/antigravity_cli/herdr-agent-state.ps1"),
),
(
"claude",
include_str!("assets/claude/herdr-agent-state.ps1"),
),
("codex", include_str!("assets/codex/herdr-agent-state.ps1")),
(
"copilot",
include_str!("assets/copilot/herdr-agent-state.ps1"),
),
(
"cursor",
include_str!("assets/cursor/herdr-agent-state.ps1"),
),
("devin", include_str!("assets/devin/herdr-agent-state.ps1")),
("droid", include_str!("assets/droid/herdr-agent-state.ps1")),
("grok", include_str!("assets/grok/herdr-agent-state.ps1")),
("kimi", include_str!("assets/kimi/herdr-agent-state.ps1")),
(
"letta",
include_str!("assets/letta/herdr-agent-session.ps1"),
),
(
"mastracode",
include_str!("assets/mastracode/herdr-agent-state.ps1"),
),
(
"qodercli",
include_str!("assets/qodercli/herdr-agent-state.ps1"),
),
("qwen", include_str!("assets/qwen/herdr-agent-session.ps1")),
] {
assert!(
!asset.contains("[Console]::In.ReadToEnd()"),
"{name} hook must not decode stdin with the console input code page"
);
assert!(
asset.contains("[Console]::OpenStandardInput()")
&& asset.contains("[System.Text.Encoding]::UTF8"),
"{name} hook must decode raw stdin as UTF-8"
);
}
}
#[cfg(windows)]
#[test]
fn claude_windows_hook_reports_utf8_session_path() {
use std::io::Write;
use std::process::{Command, Stdio};
let _lock = integration_env_lock();
let base = unique_base();
let home = base.join("home");
fs::create_dir_all(home.join(".claude")).unwrap();
let previous_home = std::env::var_os("HOME");
std::env::set_var("HOME", &home);
let installed = install_claude().unwrap();
let capture = base.join("args.txt");
let fake_herdr = base.join("herdr-capture.ps1");
fs::write(
&fake_herdr,
"$capture = $env:HERDR_TEST_CAPTURE\n[System.IO.File]::WriteAllText($capture, ($args -join \"`n\"), [System.Text.UTF8Encoding]::new($false))\n",
)
.unwrap();
// A Korean profile path: decoded with a non-UTF-8 console input code page
// (CP949 on Korean Windows) it garbles the JSON and the hook exits silently.
let transcript = "C:\\Users\\\u{d55c}\u{ae00}\\transcript.jsonl";
let payload = format!(
r#"{{"hook_event_name":"SessionStart","session_id":"claude-session","transcript_path":{}}}"#,
serde_json::to_string(transcript).unwrap()
);
let mut child = Command::new("powershell")
.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-File"])
.arg(&installed.hook_path)
.arg("session")
.env("HERDR_ENV", "1")
.env("HERDR_PANE_ID", "w1:p2")
.env("HERDR_BIN_PATH", &fake_herdr)
.env("HERDR_TEST_CAPTURE", &capture)
.env_remove("CURSOR_VERSION")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child
.stdin
.take()
.unwrap()
.write_all(payload.as_bytes())
.unwrap();
let output = child.wait_with_output().unwrap();
assert!(
output.status.success(),
"hook failed: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let args = fs::read_to_string(&capture).unwrap();
assert!(args.contains("report-agent-session"), "captured: {args}");
assert!(args.contains("--agent-session-id"), "captured: {args}");
assert!(args.contains("claude-session"), "captured: {args}");
assert!(args.contains(transcript), "captured: {args}");
if let Some(home) = previous_home {
std::env::set_var("HOME", home);
} else {
std::env::remove_var("HOME");
}
let _ = fs::remove_dir_all(base);
}
#[test]
fn uninstall_claude_removes_herdr_hooks_and_preserves_others() {
let _lock = integration_env_lock();
@@ -1304,7 +1430,7 @@ fn codex_v2_integration_status_is_outdated() {
assert_eq!(codex.path, hook_path);
assert_eq!(codex.installed_version, Some(2));
assert_eq!(codex.expected_version, 9);
assert_eq!(codex.expected_version, 10);
assert_eq!(codex.state, IntegrationStatusKind::Outdated);
std::env::remove_var("HOME");
@@ -3873,7 +3999,7 @@ fn install_qwen_writes_session_hook_and_preserves_settings() {
assert!(settings.get("permissions").is_some());
let hook_asset = fs::read_to_string(&installed.hook_path).unwrap();
assert!(hook_asset.contains("HERDR_INTEGRATION_ID=qwen"));
assert!(hook_asset.contains("HERDR_INTEGRATION_VERSION=1"));
assert!(hook_asset.contains("HERDR_INTEGRATION_VERSION=2"));
assert!(hook_asset.contains("herdr:qwen"));
install_qwen().unwrap();
@@ -4251,7 +4377,7 @@ fn install_cursor_uses_cursor_config_dir_env() {
}
#[test]
fn cursor_v1_integration_status_is_current() {
fn cursor_v1_integration_status_is_outdated() {
let _lock = integration_env_lock();
let base = unique_base();
let cursor_dir = base.join(".cursor");
@@ -4269,8 +4395,9 @@ fn cursor_v1_integration_status_is_current() {
.iter()
.find(|status| status.target == crate::api::schema::IntegrationTarget::Cursor)
.expect("cursor integration status");
assert_eq!(cursor.state, IntegrationStatusKind::Current);
assert_eq!(cursor.installed_version, Some(CURSOR_INTEGRATION_VERSION));
assert_eq!(cursor.installed_version, Some(1));
assert_eq!(cursor.expected_version, CURSOR_INTEGRATION_VERSION);
assert_eq!(cursor.state, IntegrationStatusKind::Outdated);
clear_integration_path_env();
let _ = fs::remove_dir_all(base);
@@ -4762,7 +4889,7 @@ fn install_antigravity_cli_writes_hook_and_updates_hooks_json() {
}
#[test]
fn antigravity_cli_v2_install_is_outdated_until_reinstalled() {
fn antigravity_cli_v3_install_is_outdated_until_reinstalled() {
let _lock = integration_env_lock();
let base = unique_base();
let agy_dir = base.join(".gemini").join("config");
@@ -4771,7 +4898,7 @@ fn antigravity_cli_v2_install_is_outdated_until_reinstalled() {
fs::write(
hook_dir.join(ANTIGRAVITY_CLI_HOOK_INSTALL_NAME),
ANTIGRAVITY_CLI_HOOK_ASSET
.replace("HERDR_INTEGRATION_VERSION=3", "HERDR_INTEGRATION_VERSION=2"),
.replace("HERDR_INTEGRATION_VERSION=4", "HERDR_INTEGRATION_VERSION=3"),
)
.unwrap();
std::env::set_var(ANTIGRAVITY_CLI_CONFIG_DIR_ENV_VAR, &agy_dir);
@@ -4784,8 +4911,8 @@ fn antigravity_cli_v2_install_is_outdated_until_reinstalled() {
};
let outdated = status();
assert_eq!(outdated.state, IntegrationStatusKind::Outdated);
assert_eq!(outdated.installed_version, Some(2));
assert_eq!(outdated.expected_version, 3);
assert_eq!(outdated.installed_version, Some(3));
assert_eq!(outdated.expected_version, 4);
install_antigravity_cli().unwrap();
assert_eq!(status().state, IntegrationStatusKind::Current);