Skip to content
Kward

Lifecycle hooks

Lifecycle hooks are deterministic runtime callbacks around Kward's agent workflow. Use them when something must happen because of an event, not because the model remembered an instruction.

Hooks can observe events and return structured decisions to allow, deny, ask for approval, modify supported payloads, warn, retry, or defer. They are currently exposed through trusted Ruby plugins and through command hooks configured in config.json.

Hooks run as local code with your user permissions. Do not install hooks you do not trust, and do not put secrets in hook output.

Hooks versus other extension points

Need Use
Reusable prompt text Prompt template
Model instructions for a task Skill
Repository guidance AGENTS.md
New local command/UI behavior Plugin command
Deterministic policy or automation around runtime events Lifecycle hook

Decisions

A hook may return one of these decisions:

Decision Meaning
allow Continue normally. This is also the default for nil Ruby hook returns and empty command-hook output.
deny Stop the operation and return a declined result.
ask Ask the frontend for approval when an approval bridge is available; otherwise Kward treats it as denied for safety.
modify Continue with an event-specific payload update. Tool, shell, turn, and model-request hooks currently support useful modifications.
warn Continue and record a warning decision. Hook failures use the event or hook entry's configured failure policy.
retry Reserved for retry-aware lifecycle integrations.
defer Reserved for asynchronous follow-up integrations.

When multiple hooks match one event, they run by ascending order. A deny stops later hooks. A modify updates the payload seen by later hooks, but only for fields listed as supported modifications for that event. Unknown modification fields are ignored for known events.

Ruby plugin hooks

Register hooks from trusted plugin files in ~/.kward/plugins/*.rb:

Kward.plugin do |plugin|
  plugin.hook "shell_command_before",
    id: "block-release",
    description: "Prevent accidental gem releases",
    order: 10,
    failure_policy: "deny",
    match: { command_regex: "\\bgem push\\b" } do |_event, ctx|
      ctx.deny("Gem releases must use the release checklist.")
    end
end

The hook block receives:

  • event: an immutable Kward::Hooks::Event.
  • ctx: the normal plugin context plus hook decision helpers.

Decision helpers:

ctx.allow
ctx.deny("reason")
ctx.ask("confirm this action")
ctx.modify(timeout_seconds: 120)
ctx.warn("continued with warning")
ctx.retry("try again later")
ctx.defer("notify asynchronously")

Command and HTTP hooks

Command and HTTP hooks are configured in ~/.kward/config.json under hooks. Command hooks receive event JSON on stdin and return a decision JSON object on stdout.

{
  "hooks": {
    "shell_command_before": [
      {
        "id": "block-release",
        "type": "command",
        "command": "~/.kward/hooks/block-release.rb",
        "timeout_seconds": 5,
        "failure_policy": "deny",
        "match": { "command_regex": "\\bgem push\\b" }
      }
    ]
  }
}

Example command hook:

#!/usr/bin/env ruby
require "json"

event = JSON.parse($stdin.read)
command = event.fetch("payload").fetch("command", "")

if command.match?(/\bgem push\b/)
  puts JSON.dump(decision: "deny", message: "Gem releases must use the release checklist.")
else
  puts JSON.dump(decision: "allow")
end

If a command hook prints nothing, Kward treats it as allow. If it exits non-zero, times out, or prints invalid JSON, Kward applies the hook's failure_policy.

HTTP hooks send the same event JSON with POST and expect the same decision JSON in the response body:

{
  "hooks": {
    "tool_call_before": [
      {
        "id": "remote-policy",
        "type": "http",
        "url": "https://policy.example.com/kward/hooks",
        "timeout_seconds": 3,
        "failure_policy": "deny",
        "headers": { "Authorization": "Bearer ..." }
      }
    ]
  }
}

Empty HTTP response bodies are treated as allow. Non-2xx responses, timeouts, network failures, and invalid JSON apply failure_policy. Be careful with HTTP hooks: event payloads can include prompts, commands, file paths, or tool arguments. Prefer local or trusted endpoints and avoid forwarding raw payloads to third parties.

Set "async": true on command or HTTP hook entries for non-blocking notifications:

{
  "hooks": {
    "file_change_after": [
      {
        "id": "notify-indexer",
        "type": "http",
        "url": "http://127.0.0.1:9393/kward/file-change",
        "async": true
      }
    ]
  }
}

Async hooks are observe-only. Kward schedules them in the background, immediately continues with allow, and ignores returned deny, ask, and modify decisions. Use async hooks for logging, indexing, metrics, or notifications — not policy enforcement.

Failure policies

Hook failures are different from hook decisions. A hook fails when Ruby plugin code raises, a command hook exits non-zero, times out, or returns invalid JSON. Configure failure_policy on plugin hooks or command-hook entries:

Policy Behavior on hook failure
allow Continue without recording a warning.
warn Continue and record a warning. This is the default for most after/notification-style events and for command hooks without explicit policy.
deny Stop the operation. This is the default for built-in before-policy events such as shell_command_before and tool_call_before when plugin handlers raise.
ask Ask the frontend for approval when supported; otherwise fail closed.

Use deny for security or release-policy hooks. Use warn for logging, formatting, notifications, and other convenience hooks that should not block the agent.

Frontend support

Lifecycle hooks run in CLI sessions, RPC sessions, and Pan mode. RPC advertises hook support in initialize.capabilities.lifecycleHooks, including supported events, decisions, audit-log path, and intentionally unsupported hook types. Hook ask decisions for tool/file/shell paths use the existing tool-approval bridge when a frontend provides one; without an approval bridge, Kward fails closed.

Pan runs configured command hooks and trusted plugin hooks for its agent turns. It streams hook_event SSE messages after matching hooks run and hook_message messages when hook/plugin code calls ctx.say. Pan still does not expose a dedicated hook approval UI, so use deny or warn policies for Pan-facing hooks until that UI exists.

Inspect hooks

Use kward hooks ... from your shell or /hooks ... inside an interactive Kward session to inspect loaded hooks and recent hook activity:

/hooks list      # configured command hooks and plugin hooks
/hooks events    # known event names, defaults, and modifiable fields
/hooks logs      # recent audit records from logs/hooks.jsonl
/hooks doctor    # configuration diagnostics and common mistake checks
/hooks trust     # trust this workspace's .kward/hooks.json
/hooks untrust   # stop trusting this workspace's hook config

kward hooks doctor and /hooks doctor warn about unknown events, unsupported hook types, invalid failure policies, non-positive timeouts, missing command executables, and invalid HTTP hook URLs.

Workspace hooks

Workspace hook config lives at .kward/hooks.json inside a project and uses the same JSON shape as user config command hooks:

{
  "hooks": {
    "shell_command_before": [
      {
        "id": "block-release",
        "command": "./scripts/kward-block-release",
        "failure_policy": "deny",
        "match": { "command_regex": "\\bgem push\\b" }
      }
    ]
  }
}

Workspace hooks are not loaded automatically. Run /hooks trust inside the workspace to trust the current .kward/hooks.json. Kward stores a digest of the trusted file in your user config directory and automatically stops loading it when the file changes. Run /hooks trust again after reviewing the new file, or /hooks untrust to remove trust.

This protects you from cloned repositories silently executing hook commands.

Audit log

Kward writes lifecycle hook audit records to:

~/.kward/logs/hooks.jsonl

When KWARD_CONFIG_PATH=/path/to/config.json is set, the log lives beside that config file under logs/hooks.jsonl.

The audit log records hook ids, sources, events, decisions, warnings, durations, payload keys, and modified keys. It intentionally avoids writing full payload values, file contents, complete transcripts, command output, or secrets. Messages are redacted and truncated before being written.

Event shape

Every hook receives this shape:

{
  "id": "hookevt_...",
  "name": "shell_command_before",
  "phase": "before",
  "timestamp": "2026-07-06T12:00:00Z",
  "session": {},
  "turn": {},
  "workspace": { "root": "/path/to/workspace" },
  "frontend": {},
  "agent": { "provider": "codex", "model": "gpt-5.5", "reasoning": "medium" },
  "payload": {}
}

Payloads are metadata-oriented by default. Full file contents, secrets, and complete transcripts are not included by default.

Supported events

Turn and model events

Event Payload highlights Supported modifications
turn_start input, display_input input, display_input
turn_context_build_before message_count none
turn_context_build_after message_count none
model_request_before messages, tools, provider, model, reasoning request fields
turn_model_request_before same as model_request_before none currently consumed
model_response_after_parse parsed assistant message none
turn_model_response_complete parsed assistant message none
turn_end input, answer none

Session events

Event Payload highlights Supported modifications
session_create action none
session_resume action, path none
session_clone source_path, path none
session_fork source_path, path, entry_id none
session_rename old_name, new_name none
session_export_before path none
session_export_after path none
session_compact_before instructions none
session_compact_after old_message_count, new_message_count none

session_export_before and session_compact_before can deny or ask for approval. Other session events are observe-and-warn hooks for automation and audit trails.

Tool events

Event Payload highlights Supported modifications
tool_call_before tool_name, arguments, tool_call_id, source, MCP metadata when applicable arguments
tool_call_after tool metadata plus content none
tool_call_error tool metadata plus error none

MCP events

Event Payload highlights Supported modifications
mcp_tool_before tool_name, arguments, server_name, remote_name none
mcp_tool_after MCP metadata plus content none
mcp_tool_error MCP metadata plus error none

MCP events are emitted in addition to generic tool_call_* events for tools provided by configured MCP servers.

Compaction events

Event Payload highlights Supported modifications
tool_output_compact_before tool_name, bytes, duplicate none
tool_output_compact_after tool_name, bytes_before, bytes_after, compacted none
session_compact_before instructions none
session_compact_after old_message_count, new_message_count none

tool_output_compact_before can skip tool-output compaction when denied or approval is unavailable. The original tool output is still stored in the transcript metadata/artifact path as usual.

Shell events

Event Payload highlights Supported modifications
shell_command_before command, timeout_seconds, cwd command, timeout_seconds
shell_command_after shell metadata plus content none

Worker events

Event Payload highlights Supported modifications
worker_job_create worker_id, role, title, status, session_path none
worker_job_start_before worker metadata none
worker_job_start_after worker metadata none
worker_job_ready_for_review worker metadata none
worker_job_failed worker metadata plus error none

Worker hooks are observe-and-warn hooks for background worker automation and audit trails. They do not control worker scheduling.

Git events

Event Payload highlights Supported modifications
git_status_after root, status_count none
git_diff_before root, path, untracked none
git_diff_after root, path, untracked, bytes none
git_stage_before root, path, action none
git_stage_after root, path, action, success, output none
git_commit_before root, message none
git_commit_after root, message, success, output none

git_diff_before, git_stage_before, and git_commit_before can deny or ask for approval. Git hook payloads include command output for after-events, so avoid forwarding raw payloads to third-party services.

File events

Event Payload highlights Supported modifications
file_change_before tool_name, operation, path, files, plus content for writes or edits for edits none
file_change_after tool_name, operation, path, files, content none

file_change_before fires before write_file or edit_file mutates the workspace and can deny or ask for approval. file_change_after fires only after successful write_file or edit_file results.

MCP events

MCP tools are surfaced through generic tool events with:

  • source: "mcp"
  • server_name
  • remote_name

Use match selectors to target MCP tools:

{
  "match": { "mcp_server": "safari", "mcp_tool": "browser_console_messages" }
}

Match selectors

Hook entries support match selectors:

Selector Matches
event or name Event name
phase before, after, during, or error
tool or tool_name Tool name
mcp_server or server MCP server name
mcp_tool or remote_name MCP remote tool name
operation File operation such as write or edit
frontend Frontend name when supplied
provider Active model provider
model Active model
path or paths File path glob
command_regex Ruby regular expression applied to shell command

Unknown selector keys match same-named payload fields.

Security notes

  • Plugin hooks are trusted Ruby code loaded only from ~/.kward/plugins/*.rb.
  • Command hooks run local commands with your user permissions.
  • Workspace hook files are loaded only after /hooks trust, and trust is invalidated when .kward/hooks.json changes.
  • Hook payloads are intentionally bounded and metadata-oriented; avoid logging raw event JSON if your hook receives prompt or command data.
  • The built-in audit log records payload keys and redacted decision messages, not raw payload values.
  • ask decisions fail closed when no approval bridge exists.

Recipes

Block dangerous shell commands

Kward.plugin do |plugin|
  plugin.hook "shell_command_before", match: { command_regex: "\\brm\\s+-rf\\b" } do |_event, ctx|
    ctx.deny("Refusing recursive forced removal.")
  end
end

Increase timeout for test commands

Kward.plugin do |plugin|
  plugin.hook "shell_command_before", match: { command_regex: "\\b(rake|rspec|minitest)\\b" } do |_event, ctx|
    ctx.modify(timeout_seconds: 120)
  end
end

Run a formatter after Ruby edits

{
  "hooks": {
    "file_change_after": [
      {
        "id": "format-ruby",
        "command": "~/.kward/hooks/format-ruby.rb",
        "match": { "paths": ["**/*.rb"] },
        "timeout_seconds": 30
      }
    ]
  }
}

The formatter hook can inspect payload.files and run the appropriate local command itself.