Skip to content

Commit 3dbd060

Browse files
RealZSTclaude
andauthored
feat(kiro): unblock global hook install (shipped upstream in IDE 1.0.182 / CLI 2.13.0) (#131)
Kiro now loads ~/.kiro/hooks/ in every workspace (kirodotdev/Kiro#5440, closed completed 2026-07-22), so the supports_global_hook_install() override is removed and Kiro inherits the trait default like every other adapter. Scanner and deploy paths were already scope-agnostic — only the install guard was blocking. The capability guard in install_to_agent stays: it is the designed off-switch for future agents, now pinned by a dedicated stub-adapter test (NoGlobalHooksAdapter) since no shipped adapter returns false anymore. Locale copy for the dormant UI branch is reworded to be agent-neutral; the kiroGlobalHooksPending key name is frozen to avoid three-locale churn (follow-up: rename when a real agent needs the branch again). Notes for release: requires Kiro IDE >= 1.0.182 (CLI: 3.0 early access via kiro-cli --v3); Kiro merges hooks across scopes, so the same hook installed at both global and project level fires twice. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 96b5d5b commit 3dbd060

8 files changed

Lines changed: 115 additions & 48 deletions

File tree

‎crates/hk-core/src/adapter/kiro.rs‎

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -106,16 +106,6 @@ impl AgentAdapter for KiroAdapter {
106106
true
107107
}
108108

109-
/// Kiro's docs describe `~/.kiro/hooks/` (user-level), but no released
110-
/// version loads it — verified on-device with 1.0.89 (macOS): the IDE
111-
/// only counts `.kiro/hooks/` workspace hooks. Upstream:
112-
/// kirodotdev/Kiro#5440 (open feature request, Feb 2026) and
113-
/// kirodotdev/Kiro#9857 (open bug, "not loaded in 1.0.52"). Flip this
114-
/// back to `true` once upstream ships user-level loading.
115-
fn supports_global_hook_install(&self) -> bool {
116-
false
117-
}
118-
119109
fn hook_config_paths_for(&self, scope: &ConfigScope) -> Vec<PathBuf> {
120110
match scope {
121111
ConfigScope::Global => Self::json_files(&self.base_dir().join("hooks")),

‎crates/hk-core/src/adapter/mod.rs‎

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -940,23 +940,6 @@ mod tests {
940940
}
941941
}
942942

943-
#[test]
944-
fn test_supports_global_hook_install_false_only_for_kiro() {
945-
// Kiro's docs claim `~/.kiro/hooks/` (user-level) but no released
946-
// version loads it (kirodotdev/Kiro#5440, #9857; verified on 1.0.89).
947-
// Everyone else keeps the default: global hook deploy allowed.
948-
let adapters = all_adapters();
949-
for a in &adapters {
950-
let expected = a.name() != "kiro";
951-
assert_eq!(
952-
a.supports_global_hook_install(),
953-
expected,
954-
"{} supports_global_hook_install should be {expected}",
955-
a.name()
956-
);
957-
}
958-
}
959-
960943
/// The full per-agent capability matrix, every value verified against
961944
/// official docs 2026-07 (see docs/superpowers/specs/
962945
/// 2026-07-11-cross-agent-project-install-plan.md for citations).
@@ -977,8 +960,8 @@ mod tests {
977960
("copilot", true, true, false, true, true), // project hooks deferred
978961
("antigravity", true, false, false, false, true), // no MCP/project, no hooks
979962
("opencode", true, true, false, false, true), // hooks are JS plugins
980-
("kiro", true, true, true, true, false), // kirodotdev/Kiro#5440
981-
("omp", true, true, false, false, true), // hooks are JS/TS modules
963+
("kiro", true, true, true, true, true), // global hooks shipped in Kiro IDE 1.0.182 / CLI 2.13.0
964+
("omp", true, true, false, false, true), // hooks are JS/TS modules
982965
("hermes", false, false, false, true, true), // global-only (hermes-agent#4667)
983966
("dsh", true, false, false, false, true), // MCP is cordis-layer only; no own hook format
984967
("grok", true, true, true, true, true),

‎crates/hk-core/src/models.rs‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -363,8 +363,9 @@ pub struct AgentCapabilities {
363363
/// Whether the agent has a declarative hook system at all
364364
/// (`hook_format() != HookFormat::None`).
365365
pub hooks_supported: bool,
366-
/// Whether user-level (global) hook install works upstream
367-
/// (false only for Kiro today, kirodotdev/Kiro#5440).
366+
/// False when the agent cannot load user-level (global) hooks.
367+
/// No shipped adapter sets this to false today; the capability matrix
368+
/// test pins per-agent values.
368369
pub global_hook_install: bool,
369370
/// Which remote MCP transports the agent's config can express
370371
/// (derived from `AgentAdapter::remote_mcp_schema`). Gates installing

‎crates/hk-core/src/service.rs‎

Lines changed: 102 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1552,9 +1552,9 @@ pub fn install_to_agent(
15521552
entry.event = translated_event;
15531553

15541554
// Global installs bail out for agents that don't load user-level
1555-
// hooks (e.g. Kiro, kirodotdev/Kiro#5440) instead of writing a
1556-
// config that would silently never fire. Project-scope installs
1557-
// are exactly the supported alternative for those agents.
1555+
// hooks instead of writing a config that would silently never
1556+
// fire. Project-scope installs are the supported alternative for
1557+
// those agents.
15581558
if matches!(target_scope, ConfigScope::Global)
15591559
&& !target_adapter.supports_global_hook_install()
15601560
{
@@ -3391,7 +3391,7 @@ mod tests {
33913391
}
33923392

33933393
#[test]
3394-
fn test_install_to_agent_kiro_hook_project_ok_global_rejected() {
3394+
fn test_install_to_agent_kiro_hook_project_and_global_ok() {
33953395
use crate::adapter;
33963396

33973397
let dir = TempDir::new().unwrap();
@@ -3425,22 +3425,25 @@ mod tests {
34253425
Box::new(adapter::kiro::KiroAdapter::with_home(home.to_path_buf())),
34263426
];
34273427

3428-
// Global install to Kiro stays rejected (kirodotdev/Kiro#5440).
3429-
let err = install_to_agent(
3428+
// Global install lands in ~/.kiro/hooks/ (Kiro IDE 1.0.182 / CLI 2.13.0).
3429+
install_to_agent(
34303430
&store,
34313431
&adapters,
34323432
&source_id,
34333433
"kiro",
34343434
None,
34353435
&ConfigScope::Global,
34363436
)
3437-
.unwrap_err();
3437+
.unwrap();
3438+
let global_hook_file = home.join(".kiro").join("hooks").join("harnesskit.json");
3439+
let global_written = std::fs::read_to_string(&global_hook_file).unwrap();
34383440
assert!(
3439-
matches!(&err, HkError::Validation(msg) if msg.contains("per-project")),
3440-
"kiro global hook install must stay blocked, got: {err:?}"
3441+
global_written.contains("\"v1\""),
3442+
"Kiro global hook file needs version v1"
34413443
);
3444+
assert!(global_written.contains("echo hi"));
34423445

3443-
// Project install is exactly the supported alternative.
3446+
// Project install keeps working alongside it.
34443447
install_to_agent(&store, &adapters, &source_id, "kiro", None, &scope).unwrap();
34453448
let hook_file = project_dir
34463449
.join(".kiro")
@@ -3454,6 +3457,95 @@ mod tests {
34543457
assert!(written.contains("echo hi"));
34553458
}
34563459

3460+
/// No shipped adapter reports `supports_global_hook_install() == false`,
3461+
/// so the guard in `install_to_agent` would otherwise be untested. This
3462+
/// stub keeps it covered for whichever agent needs it next.
3463+
struct NoGlobalHooksAdapter {
3464+
home: std::path::PathBuf,
3465+
}
3466+
3467+
impl crate::adapter::AgentAdapter for NoGlobalHooksAdapter {
3468+
fn name(&self) -> &str {
3469+
"noglobalhooks"
3470+
}
3471+
fn base_dir(&self) -> std::path::PathBuf {
3472+
self.home.join(".noglobalhooks")
3473+
}
3474+
fn detect(&self) -> bool {
3475+
true
3476+
}
3477+
fn skill_dirs(&self) -> Vec<std::path::PathBuf> {
3478+
vec![]
3479+
}
3480+
fn mcp_config_path(&self) -> std::path::PathBuf {
3481+
self.base_dir().join("mcp.json")
3482+
}
3483+
fn hook_config_path(&self) -> std::path::PathBuf {
3484+
self.base_dir().join("hooks.json")
3485+
}
3486+
fn plugin_dirs(&self) -> Vec<std::path::PathBuf> {
3487+
vec![]
3488+
}
3489+
fn read_mcp_servers(&self) -> Vec<crate::adapter::McpServerEntry> {
3490+
vec![]
3491+
}
3492+
fn read_hooks(&self) -> Vec<crate::adapter::HookEntry> {
3493+
vec![]
3494+
}
3495+
fn supports_global_hook_install(&self) -> bool {
3496+
false
3497+
}
3498+
}
3499+
3500+
#[test]
3501+
fn test_install_to_agent_global_hook_rejected_when_unsupported() {
3502+
use crate::adapter;
3503+
3504+
let dir = TempDir::new().unwrap();
3505+
let home = dir.path();
3506+
let store = Mutex::new(Store::open(&home.join("test.db")).unwrap());
3507+
3508+
// Source: a Claude global hook in ~/.claude/settings.json.
3509+
std::fs::create_dir_all(home.join(".claude")).unwrap();
3510+
std::fs::write(
3511+
home.join(".claude").join("settings.json"),
3512+
r#"{"hooks":{"PreToolUse":[{"matcher":"Bash","hooks":[{"type":"command","command":"echo hi"}]}]}}"#,
3513+
)
3514+
.unwrap();
3515+
let hook_name = "PreToolUse:Bash:echo hi";
3516+
let source_id =
3517+
scanner::stable_id_with_scope_for(hook_name, "hook", "claude", &ConfigScope::Global);
3518+
let mut ext = make_skill(ConfigScope::Global, None);
3519+
ext.id = source_id.clone();
3520+
ext.kind = ExtensionKind::Hook;
3521+
ext.name = hook_name.into();
3522+
ext.source_path = None;
3523+
store.lock().insert_extension(&ext).unwrap();
3524+
3525+
let adapters: Vec<Box<dyn adapter::AgentAdapter>> = vec![
3526+
Box::new(adapter::claude::ClaudeAdapter::with_home(
3527+
home.to_path_buf(),
3528+
)),
3529+
Box::new(NoGlobalHooksAdapter {
3530+
home: home.to_path_buf(),
3531+
}),
3532+
];
3533+
3534+
let err = install_to_agent(
3535+
&store,
3536+
&adapters,
3537+
&source_id,
3538+
"noglobalhooks",
3539+
None,
3540+
&ConfigScope::Global,
3541+
)
3542+
.unwrap_err();
3543+
assert!(
3544+
matches!(&err, HkError::Validation(msg) if msg.contains("per-project")),
3545+
"global hook install must be blocked for agents without user-level hooks, got: {err:?}"
3546+
);
3547+
}
3548+
34573549
#[test]
34583550
fn test_install_to_agent_remote_mcp_claude_to_grok() {
34593551
use crate::adapter;

‎src/components/extensions/extension-detail.tsx‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -654,10 +654,11 @@ export function ExtensionDetail() {
654654
const hookUnsupported =
655655
group.kind === "hook" &&
656656
!agent.capabilities.hooks_supported;
657-
// Kiro loads workspace hooks but no released version
658-
// loads user-level ~/.kiro/hooks/ (kirodotdev/Kiro#5440),
659-
// so global-targeted hook installs stay blocked while
660-
// project-scope installs go through.
657+
// Blocks global-targeted hook installs for agents that
658+
// load workspace hooks only; project-scope installs still
659+
// go through. Currently every adapter reports true, so
660+
// this branch is dormant — kept because the capability
661+
// flag is the designed off-switch for future agents.
661662
const globalHookBlocked =
662663
group.kind === "hook" &&
663664
effectiveTarget?.type === "global" &&

‎src/lib/i18n/locales/en/extensions.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@
9090
"agents": "Agents",
9191
"installToAgent": "Install to Agent",
9292
"hooksNotSupported": "Hooks not supported",
93-
"kiroGlobalHooksPending": "Kiro does not load user-level (global) hooks yet (upstream kirodotdev/Kiro#5440) - install this hook per-project instead",
93+
"kiroGlobalHooksPending": "This agent doesn't load user-level (global) hooks — install this hook per-project instead",
9494
"projectScopeUnsupported": "{{agent}} doesn't support project-level {{kind}} installs yet",
9595
"remoteTransportUnsupported": "{{agent}} doesn't support {{transport}} remote MCP servers",
9696
"cliChildrenSkipped": "Skipped {{count}} bundled item(s) this agent can't install at project scope",

‎src/lib/i18n/locales/zh-TW/extensions.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@
9090
"agents": "代理",
9191
"installToAgent": "安裝到代理",
9292
"hooksNotSupported": "不支援 hook",
93-
"kiroGlobalHooksPending": "Kiro 尚未載入使用者層(全域)hook(上游 kirodotdev/Kiro#5440),請改用專案層安裝",
93+
"kiroGlobalHooksPending": "該代理不載入使用者層級(全域)hook——請將此 hook 安裝到專案層",
9494
"projectScopeUnsupported": "{{agent}} 尚不支援專案層 {{kind}} 安裝",
9595
"remoteTransportUnsupported": "{{agent}} 不支援 {{transport}} 遠端 MCP server",
9696
"cliChildrenSkipped": "已跳過 {{count}} 個該代理無法在專案層安裝的隨附項目",

‎src/lib/i18n/locales/zh/extensions.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@
9090
"agents": "智能体",
9191
"installToAgent": "安装到智能体",
9292
"hooksNotSupported": "不支持 hook",
93-
"kiroGlobalHooksPending": "Kiro 尚未加载用户级(全局)hook(上游 kirodotdev/Kiro#5440),请改用项目级安装",
93+
"kiroGlobalHooksPending": "该智能体不加载用户级(全局)hook——请将此 hook 安装到项目级",
9494
"projectScopeUnsupported": "{{agent}} 尚不支持项目级 {{kind}} 安装",
9595
"remoteTransportUnsupported": "{{agent}} 不支持 {{transport}} 远程 MCP 服务器",
9696
"cliChildrenSkipped": "已跳过 {{count}} 个该智能体无法在项目级安装的捆绑项",

0 commit comments

Comments
 (0)