Skip to content

Plugin-defined preToolUse hooks (hooks.json) do not fire - neither in main session nor subagents #2540

Open

Description

@solvaholic

Describe the bug

preToolUse hooks defined in a plugin's hooks.json file are never executed. The hooks don't fire in the main agent session or in subagents spawned via the task tool.

This is distinct from #2392, which reports that config.json-defined hooks work in the main session but not subagents. In our case, plugin-sourced hooks don't fire anywhere.

Affected version

1.0.18 (macOS)

Steps to reproduce the behavior

  1. Install a local plugin that defines a preToolUse hook in hooks.json:
    {
      "version": 1,
      "hooks": {
        "preToolUse": [
          {
            "type": "command",
            "bash": "./scripts/guardrails.sh",
            "cwd": ".",
            "timeoutSec": 10
          }
        ]
      }
    }
  2. The plugin's plugin.json references it with "hooks": "hooks.json"
  3. The guardrails script outputs {"permissionDecision": "deny", ...} for matching commands (verified by running the script manually with piped JSON input)
  4. Start a session in a project that has this plugin installed
  5. Run a bash command that should be blocked (e.g., one containing ghe-config-apply)
  6. Result: Command executes without any hook intervention
  7. Manually running the same script with the same input correctly returns a deny decision

Expected behavior

Plugin-defined preToolUse hooks should be loaded and executed for all tool calls, the same way config.json-defined hooks are.

Additional context

Activity

  1. added
    area:pluginsPlugin system, marketplace, hooks, skills, extensions, and custom agents
    on Apr 6, 2026
  2. added
    area:sessionsSession management, resume, history, session picker, and session state
    area:agentsSub-agents, fleet, autopilot, plan mode, background agents, and custom agents
    on Apr 6, 2026
  3. Smfakhoury commented on Apr 23, 2026

    @Smfakhoury

    +1 All hooks don't seem to work, pre and post skill use, for plugins installed via marketplace. The same hooks DO fire if the hooks directory is manually copied into the agent/repo cwd.

  4. pycloux commented on Apr 26, 2026

    @pycloux

    +1 seems to be the same for VSCode : microsoft/vscode#311238 (comment)

  5. brunobastosg commented on May 28, 2026

    @brunobastosg

    I have the same problem. In my case, the plugin has a agentStop hook, which is never fired.

  6. solvaholic commented on Jun 8, 2026

    @solvaholic
    Author

    Adding a likely root cause and a workaround.

    #3659 ("CLI cannot execute hooks shipped with plugins") looks like the same defect. @brandonh-msft traced it there: a plugin's hooks.json points at its script with a path relative to cwd: ".", but once installed the plugin lives under $COPILOT_HOME/cache/marketplaces/<repo-slug>/plugins/<plugin>. The CLI resolves the hook command against the project cwd rather than the plugin root, so the script isn't found and the hook never runs.

    That explains why this looked like "hooks don't fire at all" on 1.0.18: back then a failed preToolUse hook was silently allowed. As of 1.0.57 those errors deny instead of silently passing (changelog 1.0.57), so the same failure now surfaces loudly in #3659 instead of looking like a no-op.

    Workaround (works as of 1.0.60): set "cwd": "${PLUGIN_ROOT}" in the plugin's hooks.json, since cwd expansion of plugin vars landed in 1.0.60 (PLUGIN_ROOT itself has been injected since 1.0.26).

    Suggested fix, same as #3659's checklist:

    • Default a plugin-sourced hook's cwd to its plugin root when cwd is unspecified, and resolve relative bash/powershell/File paths against PLUGIN_ROOT — plugin authors can't know the install path, so relative paths need to anchor to the plugin root by default (the way skills/commands in the same plugin already resolve).
    • Document PLUGIN_ROOT.

    Suggest deduping against #3659, which carries the fuller diagnosis.

  7. Harihara04sudhan commented on Jun 9, 2026

    @Harihara04sudhan

    Still reproducing on 1.0.60 (42 patch releases after this was first reported on 1.0.18). And in our case, none of the 8 hook event types fire — not just preToolUse. Posting evidence in case it helps maintainers triage.

    Environment

    Copilot CLI version 1.0.60
    OS / shell macOS 25.5.0, zsh
    Plugin source Installed via copilot plugin install from a public marketplace repo
    Plugin runtime Node.js 25.8.1 (homebrew)

    Plugin manifest

    plugin.json (under .claude-plugin/):

    {
      "name": "armorcopilot",
      "version": "0.1.0",
      "hooks": "./hooks/hooks.json",
      "mcpServers": "./.mcp.json"
    }

    hooks/hooks.json (all 8 event types declared):

    {
      "version": 1,
      "hooks": {
        "sessionStart": [{"type":"command","bash":"node \"$CLAUDE_PLUGIN_ROOT/scripts/bootstrap.mjs\" router","env":{"COPILOT_HOOK_EVENT":"SessionStart"},"timeoutSec":30}],
        "userPromptSubmitted": [{...same shape, COPILOT_HOOK_EVENT: UserPromptSubmit ...}],
        "preToolUse": [{...PreToolUse...}],
        "permissionRequest": [{...PermissionRequest...}],
        "postToolUse": [{...PostToolUse...}],
        "postToolUseFailure": [{...PostToolUseFailure...}],
        "agentStop": [{...Stop...}],
        "sessionEnd": [{...SessionEnd...}]
      }
    }

    The bash command unwraps to a plain node script.mjs router invocation. Manually firing it works fine (returns proper {"hookSpecificOutput":{"permissionDecision":"deny",...}} JSON on stdout).

    Evidence the hooks never fire

    1. Manual invocation works

    $ echo '{"toolName":"Read","toolArgs":{"path":"/tmp/x"},"sessionId":"sim"}' | \
        CLAUDE_PLUGIN_ROOT=~/.copilot/installed-plugins/armoriq/armorcopilot \
        COPILOT_HOOK_EVENT=PreToolUse \
        node ~/.copilot/installed-plugins/armoriq/armorcopilot/scripts/bootstrap.mjs router
    
    {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny",
     "permissionDecisionReason":"ArmorIQ intent drift: tool not in plan (Read)"}}

    The hook returns deny when fed a representative payload.

    2. Real Copilot sessions: zero hook activity in logs

    grep -iE 'hookspec|hookresult|hook event|exec hook|spawn hook|preToolUse|userPromptSubmit' ~/.copilot/logs/process-*.log across all logs from 1.0.59 and 1.0.60 sessions on this machine returns zero matches.

    Compare to MCP activity in the same logs, which is heavily logged:

    2026-06-09T05:56:31.325Z [ERROR] Starting MCP client for armorcopilot-policy with...
    2026-06-09T05:56:31.511Z [ERROR] MCP client for armorcopilot-policy connected, took 185ms
    2026-06-09T05:56:55.567Z [ERROR] [mcp server armorcopilot-policy stderr] ArmorIQ SDK initialized...
    2026-06-09T05:56:58.170Z [ERROR] [mcp server armorcopilot-policy stderr] Intent token issued...
    

    MCP runs cleanly. Hooks are completely silent — not even an error indicating Copilot tried to spawn them.

    3. Audit dir from plugin side stays empty

    Our plugin appends a row to a local WAL on every hook fire (postToolUse). After a full session with Read / Edit / Bash tool calls, ~/.copilot/armorcopilot/audit/ has only an empty archive/ subdir. The plugin never received a hook event.

    4. Plugin is correctly installed + enabled

    $ copilot plugin list
    Installed plugins:
      • armorcopilot@armoriq (v0.1.0)

    ~/.copilot/config.json shows "enabled": true for the plugin.

    Steps to reproduce

    # 1. install the plugin (public repo, MIT)
    copilot plugin marketplace add armoriq/armorCopilot
    copilot plugin install armorcopilot@armoriq
    
    # 2. open a session
    copilot
    
    # 3. inside the session, ask Copilot to do anything that should trigger preToolUse
    > read README.md and summarize it
    
    # 4. observe: Read fires, no hook intervention, no log entries about hook execution

    What works (so it's not a complete plugin failure)

    • The plugin's MCP server (armorcopilot-policy) starts cleanly, takes ~185ms to connect.
    • All MCP tools (register_intent_plan, policy_read, policy_update) are callable from chat.
    • MCP server stderr is logged at ERROR level so we can see SDK init, plan capture, intent token issuance.

    Only the hook system is silently no-op.

    Public reproducer

    Full plugin source: https://github.com/armoriq/armorCopilot
    Just the hooks.json: https://github.com/armoriq/armorCopilot/blob/main/plugins/armorcopilot/hooks/hooks.json

    Happy to provide a full debug-level log if you can point at a specific reproduction-time logging command — --log-level debug does not reveal any hook-attempt entries either.

  8. Harihara04sudhan commented on Jun 9, 2026

    @Harihara04sudhan

    Follow-up with stronger evidence: Copilot CLI 1.0.60's session-start banner explicitly confirms it discovers and loads all 8 hooks from our plugin's hooks.json — they're parsed, counted, and registered. They just never execute.

    Banner at session start:

    Loading: 8 hooks, 3 skills, 1 MCP server, 1 plugin

    So the bug is even more narrowly scoped than the original issue suggested:

    Stage Status
    Plugin discovery from installed-plugins/<marketplace>/<plugin>/ ✅ works (1 plugin)
    plugin.json parsing ✅ works
    hooks.json parsing — 8 hooks counted, matches the 8 entries in our file ✅ works
    MCP server registration ✅ works (1 MCP server)
    Skills registration ✅ works (3 skills)
    Hook executor invoking loaded hook entries during tool calls ❌ silently no-op

    Copilot's runtime knows the hooks exist. It just doesn't fire them when tool calls happen. Same plugin's MCP server gets invoked normally (we see it in logs and chat) — so the issue is specific to the hook execution path, not plugin runtime in general.

    Hopefully this helps narrow the search. Happy to provide more session-level state if useful.

  9. Harihara04sudhan commented on Jun 9, 2026

    @Harihara04sudhan

    Bisected to a specific regression: working on 1.0.59, broken on 1.0.60.

    We have logs from the same machine + same plugin + same prompts on both versions, 31 minutes apart on the same day:

    1.0.59 session
    (2026-06-05 16:11 UTC)
    1.0.60 session
    (today, 2026-06-09)
    Plugin discovery Loading: 8 hooks, 3 skills, 1 MCP server, 1 plugin ✅ ✅
    MCP server connects (~185-360ms) ✅ ✅
    userPromptSubmitted hook additionalContext injected into Copilot's planner ✅ ❌
    register_intent_plan MCP tool invoked every prompt (as our hook directs) ✅ — log shows Capturing plan: prompt=Show active security rules / prompt=Block all web fetches / prompt=... for every user message ❌ — fires only on the first prompt of a new session, never again
    preToolUse enforcement on raw Read / Bash tool calls ✅ — fetch blocked by policy rule ❌ — Read / Bash / Edit run unblocked

    1.0.60 published 2026-06-05 16:42 UTC, 31 minutes after our last working session. Auto-update between Friday afternoon and Monday introduced the regression.

    Concrete logs from the 1.0.59 (working) session

    2026-06-05T16:13:51 [stderr] Capturing plan: llm=github-copilot, prompt=Show active security rules
    2026-06-05T16:14:10 [stderr] Capturing plan: llm=github-copilot, prompt=Block all web fetches
    2026-06-05T16:15:12 [stderr] Capturing plan: llm=github-copilot, prompt=Show active security rules
    

    Each Capturing plan line is stderr from the plugin's MCP server when register_intent_plan is called. On 1.0.59 it appeared for every user prompt (proving hook→additionalContext→planner→MCP tool was wired). On 1.0.60 it appears only once per session.

    Workaround

    Downgrade:

    npm install -g @github/copilot@1.0.59

    After downgrade, the same plugin + same flow works end-to-end again.

    Suspect commits

    Whichever PR between 1.0.59 and 1.0.60 touched:

    • hook output parsing (specifically hookSpecificOutput.additionalContext for userPromptSubmitted)
    • the planner's context-injection path

    is the likely culprit. Happy to test specific candidate builds if you have prereleases between 1.0.59 and 1.0.60-0.

  10. patniko commented on Oct 26, 2026

    @patniko
    Collaborator

    Thanks for the report. We've shipped fixes in this area since this was filed (most recently in Copilot CLI v1.0.96), and we believe this may now be resolved. Could you retest on the latest release (Copilot CLI v1.0.103) and let us know whether you still see it? If we don't hear back in a couple of weeks, we may close this issue — you can always reopen it.

    Triaged as part of a Copilot SDK backlog review, with help from GitHub Copilot.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:agentsSub-agents, fleet, autopilot, plan mode, background agents, and custom agentsarea:pluginsPlugin system, marketplace, hooks, skills, extensions, and custom agentsarea:sessionsSession management, resume, history, session picker, and session statemore-info-needed

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions