feat: add agent lifecycle hooks (Claude Code-compatible config)#7411
Draft
tlongwell-block wants to merge 3 commits intomainfrom
Draft
feat: add agent lifecycle hooks (Claude Code-compatible config)#7411tlongwell-block wants to merge 3 commits intomainfrom
tlongwell-block wants to merge 3 commits intomainfrom
Conversation
78bd7d1 to
b9ec8bd
Compare
Hooks fire at key agent lifecycle events and execute user-configured actions via MCP tools. Supports blocking (prompt submit, tool use) and context injection (PostCompact). - 8 of 16 events wired: SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, PostToolUseFailure, PreCompact, PostCompact, Stop - Two action types: command (via developer__shell) and mcp_tool - Config reads .goose/settings.json, .claude/settings.json, or ~/.config/goose/hooks.json with forward-compatible parsing - Fail-open: hook errors never crash the agent
b9ec8bd to
8a2bbf7
Compare
Addresses issues identified in multi-round crossfire review:
- Remove Option<Hooks> wrapping; Hooks::load() returns empty default on
error, eliminating 12 if-let-Some guards across agent integration
- Extract run_post_compact_hook() helper to deduplicate 3 identical
PostCompact context-injection blocks
- Fix SessionStart to fire once per session (first user message) instead
of on every reply
- Thread parent CancellationToken through all hooks.run() calls so
session cancellation properly cancels running hooks
- Standardize blocked-tool errors on Err(ErrorData) — Ok(CallToolResult
{is_error:true}) was silently treated as success by LLM formatters
- Tighten tool name matching from contains() to exact equality
- Replace hand-rolled trailing-* glob with glob crate for full pattern
support
- Change manual_compact from Option<bool> to bool (always set for
compact events, defaults false otherwise)
- Delete unused Hooks::fire() method (all call sites use run())
0976d80 to
e4b822b
Compare
Claude Code supports "prompt" and "agent" hook action types that goose doesn't implement yet. Previously, a .claude/settings.json containing these types would fail serde deserialization entirely, silently dropping all hooks in the file — including valid "command" hooks. Adds a custom deserializer for HookEventConfig.hooks that parses each action individually: - Known types (command, mcp_tool): deserialize normally, warn on malformed config with the actual error - Unknown types (prompt, agent, etc.): warn and skip - Missing type field: warn and skip This matches the existing pattern in HookSettingsFile where unknown event names are warned and skipped rather than causing parse failures.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds a hooks system for agent lifecycle events (session start, prompt submit, tool use,
compaction, stop) that executes user-configured actions via MCP tools. Hooks can block
prompt submission or tool execution, and PostCompact hooks can inject additional context
back into the conversation after compaction.
The motivating use case: PostCompact hooks that re-inject project context after
auto-compaction, so long sessions don't lose critical knowledge when history is
summarized.
Configuration
Hooks are configured under a top-level
hookskey in:.goose/settings.json(preferred) or.claude/settings.json~/.config/goose/hooks.jsonGlobal and project configs are merged by appending project hooks after global hooks
for the same event. If both
.goose/and.claude/project configs exist,.goose/takes precedence (with a warning logged).
{ "hooks": { "PostCompact": [ { "hooks": [{ "type": "command", "command": "cat PROJECT_CONTEXT.md" }] }, { "hooks": [{ "type": "mcp_tool", "tool": "load", "arguments": { "source": "my-project-skill" } }] } ], "PreToolUse": [{ "matcher": "Bash(rm -rf*)", "hooks": [{ "type": "command", "command": "./guard.sh" }] }] } }Action types
command— runs a shell command via thedeveloper__shellMCP tool. Hookinvocation JSON is piped to stdin. To block a blockable event, exit with code 2,
or exit 0 and print
{"decision":"block"}as the sole stdout. To inject context,exit 0 and print
{"additionalContext":"..."}. Non-JSON stdout from exit-0commands is surfaced as additional context (capped at 32KB, UTF-8 safe).
mcp_tool— calls any MCP tool directly with static arguments from config.Both support an optional
timeoutfield (default: 600 seconds).Matchers
Matchers are optional per-hook-config filters:
BashorBash(pattern)— Claude Code-style; maps todeveloper__shellwith optional glob matching against the command string (full glob syntax via the
globcrate:*,?,[...]).(e.g.,
"developer__shell","slack__post_message")."auto"/"manual"— for PreCompact/PostCompact, matches the compaction trigger.Events wired in this PR (8 of 16)
/compact)additionalContextreturned, injected as agent-only messageRemaining events are defined but not yet fired: PermissionRequest, Notification,
SubagentStart, SubagentStop, SessionEnd, TeammateIdle, TaskCompleted, ConfigChange.
Design
ExtensionManager.dispatch_tool_call(),the same path the agent uses for normal tool calls. No subprocess spawning in goose core.
continue. A misconfigured hook should never crash a session.
CancellationToken, socancelling a session properly cancels any running hooks.
skipped, not parse errors. Configs from newer Claude Code versions won't break goose.
.goose/settings.jsonor.claude/settings.jsonin the working directory are ignored by default. Users must set
"allow_project_hooks": truein their global
~/.config/goose/hooks.jsonto enable them. This prevents malicious reposfrom auto-executing shell commands via hook configs.
~800 LOC new module (
hooks/), ~250 LOC integration acrossagent.rs,execute_commands.rs, andtool_execution.rs.Known limitations (v1)
GOOSE_HOOK_EXIT:<code>)appended to command execution, because
developer__shellreturns combined output as textwithout exit code metadata. Follow-up to add exit code metadata to the shell tool result
will remove this workaround.
mcp_toolhooks receive static arguments from config, not the hook invocation context.Command hooks receive the full invocation JSON on stdin.
additional_contextfrom hook results is only consumed by PostCompact today. Other eventsignore returned context (the hook still fires and can block, but context injection is
PostCompact-only for now).
PermissionRequestevent is defined but not yet fired. Goose has an approval UX, but wiringthe hook requires design decisions about what blocking means in that context (auto-deny?
skip the prompt?). Deferred to follow-up.