Team Hooks v1 configuration, execution model, events, and script contracts.

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 in gg_team add mode.
  • A named worktree is created or reused automatically 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

Event Fires when Notes
pre_teammate_add Before native worktree creation and session creation Blocking (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_add After canonical member-add commit Only for agent_tool source. Applies caller/target injection via typed team_hook_post_teammate_add_notice.
post_teammate_remove After canonical member-remove commit For agent_tool and ui_command sources. Injection to removed member is suppressed; surviving recipients use team_hook_post_teammate_remove_notice.
post_direct_message_send After direct message reaches delivery-complete success Only for agent_tool source. No fire on partial failure/terminal failure. Typed notice kind: team_hook_post_direct_message_send_notice.
post_broadcast_message_send After broadcast success by delivery rules Only for agent_tool source. Remove-race success is supported. Typed notice kind: team_hook_post_broadcast_message_send_notice.
post_compaction During compaction runtime handling Runtime-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,
  "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,
  "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,
  "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,
  "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,
  "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,
  "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,
  "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.