Skip to content

feat(cli): give Copilot searches graph context through a PostToolUse hook - #2801

Merged
DeusData merged 3 commits into
mainfrom
feat/copilot-posttooluse-hook
Oct 27, 2026
Merged

DeusData merged 3 commits into
mainfrom
feat/copilot-posttooluse-hook

Conversation

@DeusData

@DeusData DeusData commented Oct 25, 2026 •

Copy link
Copy Markdown
Owner

Why

Claude Code's and Codex's searches get graph context from a PreToolUse hook. GitHub Copilot CLI only had the SessionStart and SubagentStart context. Its hook contract rules out the pre-tool route (github/docs content/copilot/reference/hooks-reference.md):

  • preToolUse output can only decide permissions (permissionDecision, permissionDecisionReason, modifiedArgs), and command preToolUse hooks fail closed on errors.
  • postToolUse can return a top-level additionalContext, which is "appended to textResultForLlm so the model sees it after the tool output on the same turn".

VS Code's agent mode shares the Copilot harness and reads the same ~/.copilot/hooks manifest (code.visualstudio.com/docs/copilot/customization/hooks), so the same entry serves it.

What changes

  • Manifest. $COPILOT_HOME/hooks/codebase-memory-mcp.json adds a PascalCase PostToolUse entry, matcher Bash|Grep|Glob|bash|powershell|grep|rg|glob, running hook-augment --event PostToolUse --dialect copilot. The matcher names both the Claude names and the runtime names, because the documented PostToolUse matcher is a regex against the runtime tool name. The previous lifecycle-only manifest, as installs up to v0.12 wrote it, stays recognized as owned: install upgrades it in place, and uninstall removes either generation. A foreign same-named manifest still fails closed.
  • hook-augment, Copilot dialect. It reads either payload form (tool_name/tool_input or toolName/toolArgs), maps the runtime tool names to the Claude names (bash/powershell to Bash, grep/rg to Grep, glob to Glob), parses arguments passed as a JSON string, and answers with top-level additionalContext.
  • Fast path. Copilot's forced PostToolUse now goes through the hook client's no-op gate before executable-identity hashing. For Copilot the gate also skips tools the augmenter does not read, because VS Code's local hook parser ignores matchers and the entry can fire after every tool. Other dialects keep their gate unchanged.
  • Context header. It no longer says the search results are "below". After a post-tool hook they are above it.
  • Memory core, in its own commit. The manifest is rendered twice per install and uninstall (current and previous generation), so it is now written with cbm_json_writer_allocator() and released with cbm_free(CBM_MEM_CLASS_OTHER, ...), the frees that predate this change included. src/cli/cli.c drops from 170 to 166 raw allocator sites, and its scripts/memory-core-baseline.txt line is lowered to match. The rendered bytes are unchanged.
  • README, in its own commit. The Copilot CLI and VS Code rows name the new hook. The Codex CLI row still listed only the lifecycle pair after feat(cli): give Codex shell searches graph context through a PreToolUse hook #2719, so it now names the Codex PreToolUse Bash hook, and the hook section says how Codex and Copilot get search context.

Proof on real output

Built binaries, isolated HOME/cache/runtime, live daemon:

step result
install with the previous-generation binary manifest = sessionStart + subagentStart
install with this branch over it rc 0, upgraded in place: PostToolUse added with the matcher above, hooks: SessionStart + SubagentStart + PostToolUse searches
snake_case payload, tool_name rg, run through the manifest's own bash command 460 bytes, top-level additionalContext with the graph symbol and the follow-up line, about 137 ms
camelCase payload, toolName bash, toolArgs as a JSON string the same context
bash ls -la, and an edit call no output, about 20 to 28 ms (fast path, no identity hash)
uninstall manifest removed

Tests

  • New: cli_copilot_manifest_adds_posttooluse_search_hook pins the entry's shape beside the lifecycle pair, and that the previous rendering has no PostToolUse.
  • New: cli_copilot_install_upgrades_previous_manifest covers upgrade in place, idempotence, and uninstall of the previous manifest.
  • New: cli_hook_augment_copilot_posttooluse_search covers the rg, Claude-name and camelCase string-argument forms, the view tool staying silent, and invocation support.
  • New: cli_hook_augment_copilot_posttooluse_noop_gate covers non-search shell calls and unread tools as no-ops, searches paying full fare, and the Claude and Gemini gates unchanged.
  • Fixed fixtures: the three Copilot foreign-manifest tests now create .copilot/mcp-config.json. Without it Copilot CLI was never detected, so install and uninstall never reached the manifest, and the tests also passed with the ownership checks removed.
  • Revert checks (mutation sets, each restoring the tree byte-for-byte):
    • Manifest migration, invocation gate and no-op branch reverted: exactly the upgrade, search and no-op tests fail.
    • PostToolUse rendering dropped: exactly the manifest test fails.
    • Ownership checks off: all three foreign-manifest tests fail now (they passed before the fixture fix).
  • Local runs (macOS arm64, ASan/UBSan test runner, rebased on 619192a): cli 370 passed / 0 failed (1 Windows-only skip from fix(cli): resolve Windows CLI detection for long PATH and npm/mise/scoop shims (#221) #2381), agent_clients 39 / 0, daemon_application 87 / 0, daemon_bootstrap 28 / 0.
  • Lint: clang-format (Homebrew LLVM 22), cppcheck 2.20 (the CI lint image), the NOLINT check and the memory-core linter are clean on the final tree.
  • Linux and Windows legs were not run locally; PR CI is the gate.

Refs #509

…hook

Copilot CLI's preToolUse output can only decide permissions, and its command
hooks fail closed on errors, so the graph context that Claude Code and Codex
searches get could not ride a pre-tool hook there. postToolUse
additionalContext is appended to the tool result the model sees on the same
turn (github/docs content/copilot/reference/hooks-reference.md). VS Code's
agent mode shares the Copilot harness and reads the same ~/.copilot/hooks
manifest.

- Manifest: $COPILOT_HOME/hooks/codebase-memory-mcp.json adds a PascalCase
  PostToolUse entry, matcher "Bash|Grep|Glob|bash|powershell|grep|rg|glob",
  running hook-augment --event PostToolUse --dialect copilot. The previous
  lifecycle-only manifest, as installs up to v0.12 wrote it, stays recognized
  as owned: install upgrades it in place and uninstall removes either
  generation. A foreign manifest still fails closed.
- hook-augment: the Copilot dialect reads either payload form
  (tool_name/tool_input or toolName/toolArgs), maps runtime tool names to the
  Claude names (bash/powershell to Bash, grep/rg to Grep, glob to Glob),
  parses arguments passed as a JSON string, and answers with top-level
  additionalContext.
- Fast path: Copilot's forced PostToolUse goes through the no-op gate before
  executable-identity hashing. The gate also skips tools the augmenter does
  not read, because VS Code's local hook parser ignores matchers, so a
  non-search shell call or an edit exits in about 20 ms instead of paying the
  full round-trip of about 130 ms.
- The context header no longer says the search results are "below": after a
  post-tool hook they are above it.
- Tests: the three Copilot foreign-manifest tests now create
  .copilot/mcp-config.json. Without it Copilot CLI was never detected, so
  install and uninstall never reached the manifest, and the tests also passed
  with the ownership checks removed.

Refs #509

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
…h hooks

The Codex CLI row still listed only SessionStart and SubagentStart after the
PreToolUse Bash hook landed (#2719), and the hook section described only
Claude Code's search context. The row now names the Codex search hook, and the
section says how Codex, Copilot CLI and VS Code's Copilot runtime get search
context.

Refs #509

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The PostToolUse change rendered the manifest twice per install and uninstall
(current and previous generation) and freed both with free(), which grew the
raw allocator count in src/cli/cli.c past its baseline (lint-memory-core).
The manifest is now written with cbm_json_writer_allocator() and released with
cbm_free(CBM_MEM_CLASS_OTHER, ...), including the frees that predate the
change, so cli.c drops from 170 to 166 raw sites and its baseline is lowered
to match. The rendered bytes are unchanged, so byte-exact ownership of
manifests written by earlier installs still holds.

Refs #509

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData DeusData added enhancement New feature or request editor/integration Editor compatibility and CLI integration labels Oct 25, 2026
@DeusData
DeusData merged commit 129443a into main Oct 27, 2026
35 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

editor/integration Editor compatibility and CLI integration enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant