Skip to main content
Team Hooks let you run shell scripts on team lifecycle events and inject lifecycle context into agent sessions.

v1 contract lock (hard cut-over)

These contracts are current and authoritative for v1:
  • post_teammate_remove fires for operation_source=agent_tool and operation_source=ui_command.
  • Native worktree mode is first-class when worktree_name is provided on gg_team_manage(add).
  • gg_team_manage(add) supports both create-new and existing-worktree reuse (use_existing_worktree=true) when worktree_name is set.
  • In native worktree mode, runtime owns branch/worktree/cwd derivation; hook spawn_template_mutation.cwd does not override final native spawn cwd.
  • pre_teammate_add scripts execute with process cwd set to pre_teammate_add.template_source_session_cwd.
  • Native planning metadata fields in pre_teammate_add input are readable context only, not a control plane for native branch/cwd selection.
  • Worktree init (.agents/gg/worktree-init.sh) is a separate runtime contract from Team Hooks and is documented at Configure: Worktree Init.

Team Hooks vs worktree init

Keep these boundaries explicit:
  • pre_teammate_add runs before native worktree creation in the source session cwd (pre_teammate_add.template_source_session_cwd).
  • Worktree init runs after new native worktree creation in the new worktree cwd.
  • run_pre_teammate_add_hooks=false (Add Agent pre-hook checkbox off) bypasses hooks only; it does not disable worktree init.

Supported events

Team Hooks v1 supports exactly six events:
  • pre_teammate_add
  • post_teammate_add
  • post_teammate_remove
  • post_direct_message_send
  • post_broadcast_message_send
  • post_compaction

Config file locations

gg resolves hooks from two files:
  • User-level: ~/.agents/gg/team-hooks.json
  • Project-level: <workspace-or-session-cwd>/.agents/gg/team-hooks.json
Both can exist at the same time.

Merge and override rules

Effective hooks are merged per event using normalized hook name:
  1. Load user hooks.
  2. Load project hooks.
  3. If (event, name) collides, project entry overrides user entry.
  4. Unique names are unioned.
  5. Missing files are treated as no-op.
Hook names are normalized (trim + lowercase + separators collapsed to _) before duplicate/override matching.

Config schema

{
  "version": 1,
  "hooks": {
    "pre_teammate_add": [],
    "post_teammate_add": [],
    "post_teammate_remove": [],
    "post_direct_message_send": [],
    "post_broadcast_message_send": [],
    "post_compaction": []
  }
}
Each hook entry:
{
  "name": "worktree_setup",
  "command": "~/.agents/gg/hooks/pre-add.sh",
  "timeout_ms": 15000
}
Validation rules:
  • version must be 1.
  • name and command are required.
  • timeout_ms is optional (default 15000).
  • timeout_ms must be between 100 and 120000.
  • Unknown keys are rejected.
  • Duplicate hook names within the same event are rejected after normalization.

Execution model

Each hook runs as a shell command:
  • macOS/Linux: sh -lc "<command>"
  • Windows: cmd /C "<command>"
Runtime behavior:
  • Input payload is written as JSON to stdin.
  • Hook must write JSON to stdout.
  • Non-zero exit is a failure.
  • Empty or invalid JSON stdout is a failure.
  • Hooks for the same event run concurrently.
  • Aggregation is deterministic in effective hook order.

Trust boundary

Team hooks are trusted local shell execution:
  • Project and user hook scripts run as the desktop app user on the local machine.
  • No hook sandboxing or separate hook permission gate is added in v1.
  • Treat hook config and scripts as trusted code, not untrusted input.

Lifecycle delivery lanes

When hook output is applied, gg uses two coordinated lanes:
  • Model-context lane: text is injected with automation projection semantics (not a user message lane).
  • UI lane: gg appends typed timeline system_notice rows for lifecycle visibility.
Hook lifecycle notice kinds:
  • team_hook_pre_teammate_add_notice
  • team_hook_post_teammate_add_notice
  • team_hook_post_teammate_remove_notice
  • team_hook_post_direct_message_send_notice
  • team_hook_post_broadcast_message_send_notice
  • team_hook_post_compaction_notice
  • team_member_compaction_notice (baseline compaction notice kind used when no post-compaction override applies per recipient)

Event firing semantics

EventFires whenNotes
pre_teammate_addBefore native worktree creation and session creationBlocking (fail-closed). Runs in source session cwd (pre_teammate_add.template_source_session_cwd). Can request spawn cwd mutation for non-native adds, append onboarding text, and apply inject after spawn with team_hook_pre_teammate_add_notice. In native mode (worktree_name set), runtime-derived branch/worktree/cwd is authoritative and hook cwd mutation is ignored.
post_teammate_addAfter canonical member-add commitOnly for agent_tool source. Applies caller/target injection via typed team_hook_post_teammate_add_notice.
post_teammate_removeAfter canonical member-remove commitFor agent_tool and ui_command sources. Injection to removed member is suppressed; surviving recipients use team_hook_post_teammate_remove_notice.
post_direct_message_sendAfter direct message reaches delivery-complete successOnly for agent_tool source. No fire on partial failure/terminal failure. Typed notice kind: team_hook_post_direct_message_send_notice.
post_broadcast_message_sendAfter broadcast success by delivery rulesOnly for agent_tool source. Remove-race success is supported. Typed notice kind: team_hook_post_broadcast_message_send_notice.
post_compactionDuring compaction runtime handlingRuntime-source hook. Hook injection recipients use team_hook_post_compaction_notice. Baseline fallback recipients depend on runtime status (executed, no_hooks, resolution_failed, execution_failed) described below.

Source filtering rules

Source eligibility is event-specific:
  • Allowed for post_teammate_add, post_direct_message_send, post_broadcast_message_send: operation_source = agent_tool only.
  • Allowed for post_teammate_remove: operation_source = agent_tool and operation_source = ui_command.
  • pre_teammate_add and post_compaction are source-agnostic at runtime.
Practical effect:
  • Automation/system DMs (tool_automation) are excluded from message hooks.
  • UI command-originated member removes do trigger post_teammate_remove.
  • UI command-originated post-add/message operations remain filtered.

Delivery-complete behavior (direct and broadcast)

post_direct_message_send fires only when the message is fully delivered. post_broadcast_message_send fires on either:
  • full delivery, or
  • terminal completion where every recipient still currently in the team has been injected.
That second rule is what allows broadcast remove-race success:
  • If recipient B is removed mid-flight, recipient B no longer blocks hook success.
  • If an active remaining recipient fails delivery, hook does not fire.

Failure behavior

  • pre_teammate_add: fail-closed (operation is blocked on hook failure).
  • All post_*: fail-open (core operation already committed and does not roll back).
Fail-open does not mean silent:
  • Hook failures are logged and recorded in diagnostics.
  • Successful hook outputs can still be applied when other hooks fail.

post_compaction runtime status branches

Compaction fallback logic is intentionally branch-specific and should not be treated as one combined “no hook” behavior.
  • executed: effective hooks resolved and ran. Hook output can override recipients/message text. Override notices use team_hook_post_compaction_notice.
  • no_hooks: no effective post_compaction hooks were configured for the compacted member context. Runtime switches to creator_and_subscribers_only fallback targeting by default (the compacted member is excluded).
  • resolution_failed: hook resolution failed (for example, config load/parse issues). Runtime falls back to the canonical baseline target contract.
  • execution_failed: hooks resolved but execution failed. Runtime falls back to the canonical baseline target contract.
Canonical baseline fallback contract (used for resolution_failed and execution_failed) is not the same as no_hooks: it keeps standard baseline targeting semantics and baseline notice kind team_member_compaction_notice unless hook-produced overrides exist.

Input contract

Every hook receives this base envelope:
{
  "schema_version": 1,
  "hook_event": "post_direct_message_send",
  "event_id": "evt_123",
  "occurred_at_ms": 1772478000000,
  "team_id": "team_3",
  "correlation": {
    "correlation_id": "gg_team_op:...",
    "idempotency_key": "auto:gg_team_op:..."
  },
  "actor": {
    "session_id": "sess_lead",
    "identity_alias": "military_coral"
  },
  "subject": {
    "kind": "message",
    "message_id": "msg_8",
    "sender_agent_id": "sess_lead",
    "recipient_agent_ids": ["sess_member"]
  },
  "source": {
    "operation_source": "agent_tool",
    "ingress": "team_tool"
  }
}

Event-specific input examples

pre_teammate_add

{
  "schema_version": 1,
  "hook_event": "pre_teammate_add",
  "event_id": "pre_teammate_add:op_123",
  "occurred_at_ms": 1772478000000,
  "team_id": "team_3",
  "correlation": {
    "correlation_id": "gg_team_hook:pre_teammate_add:op_123",
    "idempotency_key": "gg_team_hook:pre_teammate_add:op_123:v1"
  },
  "actor": {
    "session_id": "sess_lead",
    "identity_alias": "military_coral"
  },
  "subject": {
    "kind": "member",
    "member_agent_ids": []
  },
  "source": {
    "operation_source": "agent_tool",
    "ingress": "team_tool"
  },
  "pre_teammate_add": {
    "caller_agent_id": "sess_lead",
    "template_source_session_id": "sess_lead",
    "native_worktree_requested": true,
    "member_title": "frontend",
    "onboarding_prompt": "Review repo docs first.",
    "model_preset": "gpt-5.3-codex",
    "requested_worktree_name": "native-feature",
    "planned_branch_name": "gg/native-feature",
    "planned_worktree_cwd": "/Users/me/.gg/worktrees/Users__me__code__repo/gg--native-feature",
    "planned_worktree_root": "/Users/me/.gg/worktrees/Users__me__code__repo",
    "planned_unified_workspace_path": "Users__me__code__repo",
    "template_source_session_cwd": "/Users/me/code/repo",
    "requested_spawn_template_mutation": {
      "cwd": "/tmp/team-a"
    }
  }
}

post_teammate_add

{
  "schema_version": 1,
  "hook_event": "post_teammate_add",
  "event_id": "evt_member_add",
  "occurred_at_ms": 1772478001234,
  "team_id": "team_3",
  "correlation": {
    "correlation_id": "gg_team_op:team_tool:member_add:seed",
    "idempotency_key": "auto:gg_team_op:team_tool:member_add:seed"
  },
  "actor": {
    "session_id": "sess_lead",
    "identity_alias": "military_coral"
  },
  "subject": {
    "kind": "member",
    "member_agent_ids": ["sess_new_member"]
  },
  "source": {
    "operation_source": "agent_tool",
    "ingress": "team_tool"
  }
}

post_teammate_remove

{
  "schema_version": 1,
  "hook_event": "post_teammate_remove",
  "event_id": "evt_member_remove",
  "occurred_at_ms": 1772478002345,
  "team_id": "team_3",
  "correlation": {
    "correlation_id": "gg_team_op:team_tool:member_remove:seed",
    "idempotency_key": "auto:gg_team_op:team_tool:member_remove:seed"
  },
  "actor": {
    "session_id": "sess_lead",
    "identity_alias": "military_coral"
  },
  "subject": {
    "kind": "member",
    "member_agent_ids": ["sess_removed_member"]
  },
  "source": {
    "operation_source": "agent_tool",
    "ingress": "team_tool"
  }
}

post_direct_message_send

{
  "schema_version": 1,
  "hook_event": "post_direct_message_send",
  "event_id": "evt_dm",
  "occurred_at_ms": 1772478003456,
  "team_id": "team_3",
  "correlation": {
    "correlation_id": "gg_team_op:team_tool:message_direct_send:seed",
    "idempotency_key": "post_direct_idem"
  },
  "actor": {
    "session_id": "sess_sender",
    "identity_alias": "military_coral"
  },
  "subject": {
    "kind": "message",
    "message_id": "msg_direct_1",
    "sender_agent_id": "sess_sender",
    "recipient_agent_ids": ["sess_recipient"]
  },
  "source": {
    "operation_source": "agent_tool",
    "ingress": "team_tool"
  }
}

post_broadcast_message_send

{
  "schema_version": 1,
  "hook_event": "post_broadcast_message_send",
  "event_id": "evt_broadcast",
  "occurred_at_ms": 1772478004567,
  "team_id": "team_3",
  "correlation": {
    "correlation_id": "gg_team_op:team_tool:message_broadcast_send:seed",
    "idempotency_key": "post_broadcast_idem"
  },
  "actor": {
    "session_id": "sess_sender",
    "identity_alias": "military_coral"
  },
  "subject": {
    "kind": "message",
    "message_id": "msg_broadcast_1",
    "sender_agent_id": "sess_sender",
    "recipient_agent_ids": ["sess_a", "sess_b", "sess_c"]
  },
  "source": {
    "operation_source": "agent_tool",
    "ingress": "team_tool"
  }
}

post_compaction

{
  "schema_version": 1,
  "hook_event": "post_compaction",
  "event_id": "evt_compaction",
  "occurred_at_ms": 1772478005678,
  "team_id": "team_3",
  "correlation": {
    "correlation_id": "gg_team_op:runtime:compaction_signal:seed",
    "idempotency_key": "auto:gg_team_op:runtime:compaction_signal:seed"
  },
  "actor": {
    "session_id": "sess_compacted",
    "identity_alias": "silver_lynx"
  },
  "subject": {
    "kind": "compaction",
    "compacted_member_session_id": "sess_compacted",
    "subscriber_agent_ids": ["sess_lead"],
    "creator_member_session_id": "sess_creator",
    "creator_member_identity_alias": "military_coral",
    "turn_id": "turn_123",
    "source": "context_compaction_item",
    "confidence": "high",
    "detected_at_ms": 1772478005678
  },
  "source": {
    "operation_source": "runtime",
    "ingress": "runtime"
  }
}

Output contract

Base output shape:
{
  "inject": {
    "caller": {
      "text": "Optional caller text"
    },
    "targets": [
      {
        "agent_id": "sess_target",
        "text": "Optional target text"
      }
    ]
  }
}

pre_teammate_add output extension

{
  "pre_teammate_add": {
    "spawn_template_mutation": {
      "cwd": "/path/to/worktree"
    },
    "onboarding_context_append_text": "Worktree initialized and ready."
  },
  "inject": {
    "caller": {
      "text": "Prepared teammate workspace."
    },
    "targets": [
      {
        "agent_id": "__new_member__",
        "text": "Use /path/to/worktree as your cwd."
      }
    ]
  }
}
Notes:
  • pre_teammate_add extension is valid only for the pre_teammate_add event.
  • inject.caller.text is applied to the caller session context after the new member session exists.
  • inject.targets[].agent_id = "__new_member__" is a reserved token for pre_teammate_add that resolves to the spawned member session after creation.
  • For pre_teammate_add, target IDs other than __new_member__ are treated as concrete session IDs.
  • If multiple pre hooks return conflicting spawn_template_mutation.cwd values, pre-hook execution fails.
  • If native_worktree_requested=true, runtime-derived native branch/worktree/cwd remains authoritative and hook-provided spawn_template_mutation.cwd is ignored for final spawn cwd selection.
  • Native planning metadata fields (requested_worktree_name, planned_branch_name, planned_worktree_cwd, planned_worktree_root, planned_unified_workspace_path) are readable by hooks but are not a control plane for native branch/cwd selection.
  • pre_teammate_add hooks execute with process cwd set to pre_teammate_add.template_source_session_cwd.
  • run_pre_teammate_add_hooks=false hard-bypasses all pre-hook side effects (cwd mutation, onboarding append text, and inject output) but does not disable worktree init for create-new native worktree adds.
  • For post_teammate_remove, runtime suppresses any injection targeted at removed member session IDs.

Applicability and suppression rules

  • Message-hook applicability is delivery-aware: post_direct_message_send requires full delivery; post_broadcast_message_send also permits terminal success when all still-eligible recipients were injected.
  • Post hook execution is source-filtered by event:
  • post_teammate_add, post_direct_message_send, post_broadcast_message_send: operation_source=agent_tool.
  • post_teammate_remove: operation_source=agent_tool and operation_source=ui_command.
  • pre_teammate_add and post_compaction are source-agnostic.
  • Recipient applicability is delivery-aware and membership-aware: recipients no longer in the team are excluded from broadcast terminal-success evaluation and post-remove target injection.

Example scripts for all six events

These are minimal shell examples. Replace paths and logic for your workflow.

1) pre_teammate_add

#!/usr/bin/env bash
set -euo pipefail

payload="$(cat)"
native_requested="$(echo "$payload" | jq -r '.pre_teammate_add.native_worktree_requested // false')"
template_cwd="$(echo "$payload" | jq -r '.pre_teammate_add.requested_spawn_template_mutation.cwd // ""')"

if [[ "$native_requested" == "true" ]]; then
  jq -nc '{
    inject: {
      caller: { text: "Native worktree requested; runtime-managed branch/cwd will be used." },
      targets: [{ agent_id: "__new_member__", text: "Runtime created your native worktree and set cwd automatically." }]
    }
  }'
  exit 0
fi

if [[ -n "$template_cwd" ]]; then
  worktree="$template_cwd"
else
  worktree="/tmp/gg-worktrees/$(date +%s)"
fi

mkdir -p "$worktree"

jq -nc --arg cwd "$worktree" '{
  pre_teammate_add: {
    spawn_template_mutation: { cwd: $cwd },
    onboarding_context_append_text: ("Workspace ready at " + $cwd)
  },
  inject: {
    caller: { text: ("Prepared workspace " + $cwd) },
    targets: [{ agent_id: "__new_member__", text: ("Use " + $cwd + " as your cwd.") }]
  }
}'

2) post_teammate_add

#!/usr/bin/env bash
set -euo pipefail
payload="$(cat)"
member="$(echo "$payload" | jq -r '.subject.member_agent_ids[0] // ""')"
jq -nc --arg member "$member" '{
  inject: {
    caller: { text: ("Added teammate: " + $member) },
    targets: [{ agent_id: $member, text: "Welcome. Start with README.md and TODOs." }]
  }
}'

3) post_teammate_remove

#!/usr/bin/env bash
set -euo pipefail
payload="$(cat)"
removed="$(echo "$payload" | jq -r '.subject.member_agent_ids[]?')"
jq -nc --arg removed "$removed" '{
  inject: {
    caller: { text: ("Removed teammate: " + $removed) }
  }
}'

4) post_direct_message_send

#!/usr/bin/env bash
set -euo pipefail
payload="$(cat)"
recipient="$(echo "$payload" | jq -r '.subject.recipient_agent_ids[0] // ""')"
jq -nc --arg recipient "$recipient" '{
  inject: {
    caller: { text: ("Direct message delivered to " + $recipient) }
  }
}'

5) post_broadcast_message_send

#!/usr/bin/env bash
set -euo pipefail
payload="$(cat)"
count="$(echo "$payload" | jq '.subject.recipient_agent_ids | length')"
jq -nc --arg count "$count" '{
  inject: {
    caller: { text: ("Broadcast delivery completed for " + $count + " recipients") }
  }
}'

6) post_compaction

#!/usr/bin/env bash
set -euo pipefail
payload="$(cat)"
compacted="$(echo "$payload" | jq -r '.subject.compacted_member_session_id')"
lead="$(echo "$payload" | jq -r '.subject.subscriber_agent_ids[0] // ""')"

jq -nc --arg compacted "$compacted" --arg lead "$lead" '{
  inject: {
    caller: { text: ("Compaction detected for " + $compacted + ". Summarize recent decisions before continuing.") },
    targets: (
      if $lead == "" then []
      else [{ agent_id: $lead, text: ("Compaction handled for " + $compacted) }]
      end
    )
  }
}'

Timeline rendering contract

Lifecycle hook notices are rendered from typed timeline system_notice rows, not from non-transport wrapper parsing. Legacy non-transport team_hook_* wrapper notices are not a supported runtime path.