Skip to content

Comments

feat: add agent lifecycle hooks (Claude Code-compatible config)#7411

Draft
tlongwell-block wants to merge 3 commits intomainfrom
hooks/claude-code-compatible
Draft

feat: add agent lifecycle hooks (Claude Code-compatible config)#7411
tlongwell-block wants to merge 3 commits intomainfrom
hooks/claude-code-compatible

Conversation

@tlongwell-block
Copy link
Collaborator

@tlongwell-block tlongwell-block commented Feb 21, 2026

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 hooks key in:

  • Project: .goose/settings.json (preferred) or .claude/settings.json
  • Global: ~/.config/goose/hooks.json

Global 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 the developer__shell MCP tool. Hook
    invocation 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-0
    commands 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 timeout field (default: 600 seconds).

Matchers

Matchers are optional per-hook-config filters:

  • Bash or Bash(pattern) — Claude Code-style; maps to developer__shell
    with optional glob matching against the command string (full glob syntax via the
    glob crate: *, ?, [...]).
  • Plain string — exact match against the goose tool name
    (e.g., "developer__shell", "slack__post_message").
  • "auto" / "manual" — for PreCompact/PostCompact, matches the compaction trigger.

Events wired in this PR (8 of 16)

Event Blocks enforced Notes
SessionStart no Fires once per session (first user message)
UserPromptSubmit yes If blocked, returns a short message and stops
PreToolUse yes Fires for both pre-approved and approval-required tools. If blocked, tool call is not dispatched
PostToolUse no Fires after successful tool call
PostToolUseFailure no Fires after failed tool call
PreCompact no Fires before compaction (auto-recovery and /compact)
PostCompact no If additionalContext returned, injected as agent-only message
Stop no Fires before reply ends; block not currently enforced

Remaining events are defined but not yet fired: PermissionRequest, Notification,
SubagentStart, SubagentStop, SessionEnd, TeammateIdle, TaskCompleted, ConfigChange.

Design

  • MCP-only execution — all hooks route through ExtensionManager.dispatch_tool_call(),
    the same path the agent uses for normal tool calls. No subprocess spawning in goose core.
  • Fail-open — hook errors (timeouts, parse failures, tool failures) log warnings and
    continue. A misconfigured hook should never crash a session.
  • Cancellation-aware — hooks receive the parent session's CancellationToken, so
    cancelling a session properly cancels any running hooks.
  • Forward-compatible config — unknown event names in settings.json are warned and
    skipped, not parse errors. Configs from newer Claude Code versions won't break goose.
  • Project hooks require opt-in — hooks from .goose/settings.json or .claude/settings.json
    in the working directory are ignored by default. Users must set "allow_project_hooks": true
    in their global ~/.config/goose/hooks.json to enable them. This prevents malicious repos
    from auto-executing shell commands via hook configs.

~800 LOC new module (hooks/), ~250 LOC integration across agent.rs,
execute_commands.rs, and tool_execution.rs.

Known limitations (v1)

  • Exit codes from command hooks are captured via a stdout marker (GOOSE_HOOK_EXIT:<code>)
    appended to command execution, because developer__shell returns combined output as text
    without exit code metadata. Follow-up to add exit code metadata to the shell tool result
    will remove this workaround.
  • mcp_tool hooks receive static arguments from config, not the hook invocation context.
    Command hooks receive the full invocation JSON on stdin.
  • additional_context from hook results is only consumed by PostCompact today. Other events
    ignore returned context (the hook still fires and can block, but context injection is
    PostCompact-only for now).
  • PermissionRequest event is defined but not yet fired. Goose has an approval UX, but wiring
    the hook requires design decisions about what blocking means in that context (auto-deny?
    skip the prompt?). Deferred to follow-up.
  • No tests in this PR — follow-up.

@tlongwell-block tlongwell-block force-pushed the hooks/claude-code-compatible branch 4 times, most recently from 78bd7d1 to b9ec8bd Compare February 21, 2026 20:38
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
@tlongwell-block tlongwell-block force-pushed the hooks/claude-code-compatible branch from b9ec8bd to 8a2bbf7 Compare February 21, 2026 20:54
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())
@tlongwell-block tlongwell-block force-pushed the hooks/claude-code-compatible branch from 0976d80 to e4b822b Compare February 22, 2026 01:56
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant