Skip to content

.github/hooks/*.json are not loaded in non-interactive (copilot -p) mode, but load correctly in interactive mode #3345

@torumakabe

Description

@torumakabe

Describe the bug

Repository-level hook files (.github/hooks/*.json and inline hooks in .github/copilot/settings.json) are not loaded when Copilot CLI is started in non-interactive print mode (copilot -p "..."). The exact same hook configuration loads and fires correctly when the CLI is started in interactive mode (TTY).

User-level hooks (~/.copilot/hooks/*.json and inline hooks in ~/.copilot/settings.json) load and fire correctly in both modes, as do plugin-defined hooks.

This is not documented as a -p-specific limitation in the Hooks reference, which lists .github/hooks/*.json as a supported source.

Affected version

  • CLI version: 1.0.49-1 (macOS, Darwin 24.x)
  • Reproducible across two independent repositories
  • Behavior unchanged with --log-level all

Steps to reproduce

  1. Create .github/hooks/pre-tool-use.json in a repo:

    {
      "version": 1,
      "hooks": {
        "preToolUse": [
          {
            "type": "command",
            "bash": "echo \"HOOK FIRED at $(date)\" >> /tmp/copilot-hook-log.txt; echo '{\"permissionDecision\":\"allow\"}'",
            "timeoutSec": 10
          }
        ]
      }
    }
  2. Run non-interactive:

    copilot -p "view README.md" --allow-all-tools

    /tmp/copilot-hook-log.txt is not created. ~/.copilot/logs/process-*.log shows only Loaded N hook(s) from N plugin(s) (user-level hooks and plugin hooks). No discovery message for .github/hooks/ appears.

  3. Run interactive (same directory, same hook file):

    copilot --allow-all-tools
    # Then type: view README.md

    /tmp/copilot-hook-log.txt is created with HOOK FIRED at .... ~/.copilot/logs/process-*.log shows the .github/hooks/ discovery and load messages.

Variations tested (all reproduce the bug in -p mode)

Variation Result in -p
Default invocation (copilot -p "...") ❌ Repo hook not loaded
Clean env (env -i HOME=$HOME PATH=$PATH copilot -p "...") ❌ Repo hook not loaded
Absolute path to repo as cwd ❌ Repo hook not loaded
--log-level all ❌ Repo hook not loaded (logs confirm absence of discovery)
preToolUse event ❌ Not loaded
permissionRequest event ❌ Not loaded
.github/copilot/settings.json with inline hooks ❌ Not loaded
Same config via ~/.copilot/hooks/*.json (user-level) ✅ Loaded and fired
Same config via ~/.copilot/settings.json (user-level inline) ✅ Loaded and fired
Plugin-defined hooks.json ✅ Loaded and fired

Expected behavior

.github/hooks/*.json and .github/copilot/settings.json should be loaded in both interactive and non-interactive (-p) modes, matching the official documentation. CI / automation pipelines that use copilot -p "..." rely on repository-level hooks for security policy enforcement, audit logging, and tool gating.

Actual behavior

Repository-level hooks are silently ignored in -p mode. There is no warning, error, or log message indicating that the hook configuration was found but skipped.

Security implication

Teams using .github/hooks/ to enforce security policies (e.g., blocking access to local credential caches, secret patterns; gating destructive shell commands; logging tool calls for audit) lose all of that protection when the CLI is invoked non-interactively. This is especially impactful for:

  • GitHub Actions workflows that run copilot -p "..."
  • Cron jobs and scheduled automation
  • CI hook test workflows (the hook tests themselves may pass via interactive simulation while the actual production -p calls bypass hooks)

The asymmetry between interactive and -p modes is also a footgun — users testing hooks interactively will see them work, then have them silently fail when deployed.

Workaround

Move all security-critical hooks to user-level (~/.copilot/hooks/*.json or ~/.copilot/settings.json) and distribute via dotfiles or setup scripts. Use --deny-tool 'kind(...)' on the CLI invocation for guaranteed enforcement, since --deny-tool is not affected by the hook loading bug.

Additional context

  • Confirmed with logging-only hooks (return {"permissionDecision":"allow"}) and policy hooks (return {"permissionDecision":"deny"}).
  • The process-*.log for a non-interactive run contains plugin and user-level hook discovery messages but never any .github/hooks/ line.
  • Sub-agent hook firing (a related issue preToolUse hooks are not enforced in subagents #2392) was correctly fixed in v1.0.49-0 — this -p-mode loading bug is a separate, narrower problem on the discovery/configuration-load path, not on the runtime dispatch.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:configurationConfig files, instruction files, settings, and environment variablesarea:non-interactiveNon-interactive mode (-p), CI/CD, ACP protocol, and headless automationarea:pluginsPlugin system, marketplace, hooks, skills, extensions, and custom agents

    Type

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions