-
Notifications
You must be signed in to change notification settings - Fork 35
t1898: PostToolUse hook on mcp_task to auto-record child subagent tokens via record-child #17511
Description
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 existingtoolExecuteAfter()(see diff below)EDIT: .agents/scripts/install-hooks-helper.sh:97-133— add PostToolUse registration toconfigure_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.mjsAcceptance Criteria
- New file
.agents/hooks/mcp_task_post_hook.pyexists and passespython3 -c "import ast; ast.parse(...)" -
index.mjstoolExecuteAfter()has mcp_task block and passesnode --check -
install-hooks-helper.shregisters PostToolUse in settings.json - ShellCheck clean on install-hooks-helper.sh