Skip to content

t1898: PostToolUse hook on mcp_task to auto-record child subagent tokens via record-child #17511

@marcusquinn

Description

@marcusquinn

Task

Add a PostToolUse hook on mcp_task (Task tool) that automatically calls gh-signature-helper.sh record-child after each subagent completes, recording child token usage in the parent session's ledger.

Problem

When a parent session spawns subagents via the Task tool, the child session's token consumption is invisible to the parent's signature footer. The record-child command exists in gh-signature-helper.sh and the ledger aggregation works (t1897), but there is no automated trigger — callers must manually invoke record-child after every Task call, which they rarely do. This means signature footers undercount actual token usage.

Worker Guidance

Files to Modify

  • NEW: .agents/hooks/mcp_task_post_hook.py — Claude Code PostToolUse hook (see skeleton below)
  • EDIT: .agents/plugins/opencode-aidevops/index.mjs:893-912 — add mcp_task block to existing toolExecuteAfter() (see diff below)
  • EDIT: .agents/scripts/install-hooks-helper.sh:97-133 — add PostToolUse registration to configure_claude_settings() (see diff below)

Code Skeleton: .agents/hooks/mcp_task_post_hook.py

Write this file — it's the complete implementation:

#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
# SPDX-FileCopyrightText: 2025-2026 Marcus Quinn
"""
PostToolUse hook for Claude Code: auto-record child subagent tokens (GH#17511).

Fires after every tool call. Filters for mcp_task (Task tool) completions,
extracts the child session's task_id, and calls gh-signature-helper.sh record-child.

Installed by: install-hooks-helper.sh
Location: ~/.aidevops/hooks/mcp_task_post_hook.py
Configured in: ~/.claude/settings.json (hooks.PostToolUse)

Exit behavior:
  - Exit 0 with no output = allow (post-hooks cannot block)
"""
import json
import os
import subprocess
import sys


def main():
    try:
        data = json.loads(sys.stdin.read())
    except (json.JSONDecodeError, EOFError):
        return

    tool_name = data.get("tool_name", "")
    if tool_name != "mcp_task":
        return

    # The Task tool returns task_id in its output
    tool_output = data.get("tool_output", {})
    if isinstance(tool_output, str):
        try:
            tool_output = json.loads(tool_output)
        except (json.JSONDecodeError, ValueError):
            pass

    task_id = ""
    if isinstance(tool_output, dict):
        task_id = tool_output.get("task_id", "") or tool_output.get("taskId", "")

    if not task_id:
        return

    # Fire-and-forget: record child session tokens in parent ledger
    helper = os.path.expanduser("~/.aidevops/agents/scripts/gh-signature-helper.sh")
    if os.path.isfile(helper):
        try:
            subprocess.Popen(
                [helper, "record-child", "--child", str(task_id)],
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
            )
        except OSError:
            pass


if __name__ == "__main__":
    main()

Code Diff: index.mjs — add after line 911

Insert this block inside toolExecuteAfter(), after the recordToolCall line and before the closing }:

  // GH#17511: auto-record child subagent tokens after Task tool calls
  if (toolName === "mcp_task" || toolName === "task") {
    const taskId = output?.metadata?.task_id || "";
    if (taskId) {
      const helper = join(SCRIPTS_DIR, "gh-signature-helper.sh");
      if (existsSync(helper)) {
        execFile(helper, ["record-child", "--child", taskId], (err) => {
          if (err) qualityLog("WARN", `record-child failed: ${err.message}`);
        });
      }
    }
  }

Also add execFile to the imports at the top of the file. Find the existing import { ... } from "node:child_process" or add:

import { execFile } from "node:child_process";

Code Diff: install-hooks-helper.sh — modify configure_claude_settings()

In the settings dict (line 104), add a PostToolUse entry alongside the existing PreToolUse:

settings = {
    'hooks': {
        'PreToolUse': [
            {
                'matcher': 'Bash',
                'hooks': [{'type': 'command', 'command': '$HOOK_COMMAND'}]
            }
        ],
        'PostToolUse': [
            {
                'matcher': 'Task',
                'hooks': [{'type': 'command', 'command': '$HOME/.aidevops/hooks/mcp_task_post_hook.py'}]
            }
        ]
    }
}

In the merge section (line 152-174), add equivalent logic for PostToolUse — same pattern as the PreToolUse merge but with 'PostToolUse' key and 'Task' matcher.

In install_hook() (line 57-95), add a copy for the new hook file:

cp "$source_post_hook" "$HOOKS_DIR/mcp_task_post_hook.py"
chmod +x "$HOOKS_DIR/mcp_task_post_hook.py"

Where source_post_hook is found using the same pattern as find_source_hook() but for mcp_task_post_hook.py.

Verification

# Python syntax check:
python3 -c "import ast; ast.parse(open('.agents/hooks/mcp_task_post_hook.py').read())"

# JS syntax check:
node --check .agents/plugins/opencode-aidevops/index.mjs

# Hook registration:
grep -q "PostToolUse" .agents/scripts/install-hooks-helper.sh

# record-child integration:
grep -q "record-child.*--child" .agents/hooks/mcp_task_post_hook.py
grep -q "record-child" .agents/plugins/opencode-aidevops/index.mjs

Acceptance Criteria

  • New file .agents/hooks/mcp_task_post_hook.py exists and passes python3 -c "import ast; ast.parse(...)"
  • index.mjs toolExecuteAfter() has mcp_task block and passes node --check
  • install-hooks-helper.sh registers PostToolUse in settings.json
  • ShellCheck clean on install-hooks-helper.sh

Metadata

Metadata

Assignees

Labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions