use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum HookEvent {
PreToolUse,
PostToolUse,
PostToolUseFailure,
UserPromptSubmit,
Stop,
StopFailure,
SessionStart,
SessionEnd,
SubagentStart,
SubagentStop,
PreCompact,
PostCompact,
Notification,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HookConfig {
pub event: HookEvent,
pub command: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub matcher: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout: Option<u64>,
}
#[derive(Debug, Clone, Default)]
pub struct ProjectHookDefs {
pub hooks: Vec<HookConfig>,
pub scripts: Vec<(String, String)>, }
pub fn default_project_hooks() -> ProjectHookDefs {
let mut defs = ProjectHookDefs::default();
defs.hooks.push(HookConfig {
event: HookEvent::PreToolUse,
command: ".kimi/hooks/safety-check.sh".to_string(),
matcher: Some("WriteFile|StrReplaceFile".to_string()),
timeout: Some(10),
});
defs.scripts.push((
"safety-check.sh".to_string(),
r#"#!/bin/bash
# OMK Safety Hook — blocks edits to sensitive files
# Receives JSON via stdin
set -e
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
BLOCKED="\.env$ \.env\.local$ id_rsa id_dsa \.p12$ \.key$"
for pattern in $BLOCKED; do
if echo "$FILE" | grep -Eq "$pattern"; then
echo "🛡️ OMK safety hook: blocking edit to sensitive file: $FILE" >&2
exit 2
fi
done
exit 0
"#
.to_string(),
));
defs.hooks.push(HookConfig {
event: HookEvent::Stop,
command: ".kimi/hooks/completion-check.sh".to_string(),
matcher: None,
timeout: Some(60),
});
defs.scripts.push((
"completion-check.sh".to_string(),
r#"#!/bin/bash
# OMK Completion Hook — verify gates before Kimi considers a turn complete
# Receives JSON via stdin
set -e
INPUT=$(cat)
CWD=$(echo "$INPUT" | jq -r '.cwd // empty')
# Run verification gates if .omk/gates.toml exists
if [ -f "$CWD/.omk/gates.toml" ]; then
echo "🔍 OMK completion hook: running verification gates..."
# Gates are managed by OMK runtime, this hook just logs for now
fi
exit 0
"#
.to_string(),
));
defs.hooks.push(HookConfig {
event: HookEvent::SubagentStart,
command: ".kimi/hooks/notify.sh".to_string(),
matcher: None,
timeout: Some(5),
});
defs.hooks.push(HookConfig {
event: HookEvent::SubagentStop,
command: ".kimi/hooks/notify.sh".to_string(),
matcher: None,
timeout: Some(5),
});
defs.scripts.push((
"notify.sh".to_string(),
r#"#!/bin/bash
# OMK Notification Hook — logs subagent lifecycle events
# Receives JSON via stdin
set -e
INPUT=$(cat)
EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // empty')
AGENT=$(echo "$INPUT" | jq -r '.agent_name // empty')
# Append to OMK event log
LOG_DIR="${OMK_STATE_DIR:-$HOME/.omk/state}/events"
mkdir -p "$LOG_DIR"
echo "$(date -Iseconds) $EVENT agent=$AGENT" >> "$LOG_DIR/kimi-hooks.log"
exit 0
"#
.to_string(),
));
defs
}