Skip to content

fix: gate Copilot PreToolUse feed hooks - #6577

Closed
austinywang wants to merge 55 commits into
mainfrom
issue-6574-copilot-pretooluse-hook
Closed

austinywang wants to merge 55 commits into
mainfrom
issue-6574-copilot-pretooluse-hook

Conversation

@austinywang

@austinywang austinywang commented Jun 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Closes #6574

Root cause: Copilot PreToolUse feed events were not treated as Copilot permission-gate events end to end. The feed card could fall back to telemetry/non-Copilot response serialization, and resolved decisions were emitted in Claude-style hookSpecificOutput JSON that Copilot does not process. The installed Copilot hook file also still targeted the old config path without the required hooks version.

Fix: classify Copilot PreToolUse as a blocking Feed approval request, serialize resolved Copilot decisions as top-level permissionDecision: allow|deny, and install Copilot hooks to ~/.copilot/hooks/cmux.json with version: 1.

Testing

  • Added failing-first regression coverage for Copilot PreToolUse classification, install path/version, and Copilot hook stdout decision JSON.
  • Did not run local tests or builds per branch instructions; CI is the validation gate.

Notes

  • Confirmed GitHub Copilot hook docs specify preToolUse uses top-level permissionDecision (allow, deny, or ask).

View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Gates GitHub Copilot preToolUse as a blocking approval and returns a top-level permissionDecision. Installs v1 hooks at ~/.copilot/hooks/cmux.json using Copilot’s JSON schema; addresses #6574.

  • Bug Fixes

    • Install Copilot hooks for permissionRequest and gated preToolUse at ~/.copilot/hooks/cmux.json (v1) with direct {"type":"command","bash","timeoutSec"} entries and a matcher for side‑effecting tools and ask_user; keep camelCase event names; remove stale telemetry hooks; preserve user hooks; update docs and samples.
    • Classify events: treat permissionRequest and actionable preToolUse as approvals; read‑only preToolUse is telemetry. Keep permissionRequest for existing installs and policy denials; support --telemetry-only.
    • Emit decisions: for preToolUse output top‑level permissionDecision with a localized deny reason. For permissionRequest denials emit {"behavior":"deny","message":…}; approvals are no‑ops.
    • Fail closed on errors/timeouts, when the wrapper can’t run cmux, or when the Feed socket is missing; add 120s + 5s slack to preToolUse. Route errorOccurred via notification with structured error parsing. Disable persistent permission modes and bypass for Copilot in UI and backend.
    • Tests: add Copilot hook feed integration; migrate affected suites to Swift Testing with legacy wait/assert helpers.
  • Migration

    • Run: cmux hooks copilot install to write ~/.copilot/hooks/cmux.json.

Written for commit cb1ea2d. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Improved GitHub Copilot hook permission gating so Copilot PreToolUse routes through actionable approval/deny handling.
    • Copilot hook configuration now supports a Copilot-specific JSON hook format, updates cmux hook versioning, and auto-creates the hooks directory when missing.
  • Documentation
    • Updated Copilot hook install path from ~/.copilot/config.json to ~/.copilot/hooks/cmux.json across docs.
  • Bug Fixes
    • Adjusted Copilot permission decision behavior (including localized deny reasons) and tightened Copilot permission-mode capabilities.
  • Tests
    • Added regression and integration tests covering Copilot hook installation and permission-decision request/response behavior.

@vercel

vercel Bot commented Jun 22, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 22, 2026 5:04pm
cmux-staging Building Building Preview, Comment Jun 22, 2026 5:04pm

@coderabbitai

coderabbitai Bot commented Jun 22, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The Copilot hook configuration is moved from ~/.copilot/config.json to ~/.copilot/hooks/cmux.json with automatic directory creation. A new copilotJSON(timeoutSeconds:) format variant is added to AgentHookDef.HookFormat. FeedEventClassifier explicitly registers Copilot PreToolUse events as actionable approval requests. Permission decision encoding branches on source == "copilot" to emit Copilot-format allow/deny payloads with a localized denial reason. Integration tests validate the install layout and permission flow, unit tests confirm classification semantics, and documentation is updated throughout.

Changes

Copilot PreToolUse Approval Gating

Layer / File(s) Summary
HookFormat variant and Copilot agent definition
CLI/CMUXCLI+AgentHookDefinitions.swift
Adds copilotJSON(timeoutSeconds:) enum case to AgentHookDef.HookFormat. Copilot agent updated to use .copilot/hooks directory, cmux.json filename, hooks subpath for COPILOT_HOME override, automatic directory creation, and copilotJSON(timeoutSeconds: 5) format.
PreToolUse as actionable approval request
CLI/FeedEventClassifier.swift
Registers Copilot PreToolUse in feedEventSemanticRegistry as approvalRequest. Adds source == "copilot" special-case in wire mapping to emit PermissionRequest with isActionable = true. Updates .approvalRequest documentation to clarify toolName-based resolution.
Feed bridge hook generation for copilotJSON format
CLI/cmux.swift
Adds .copilotJSON case in hook group-building switch to append copilot entries via copilotHookEntry helper. Computes copilot timeout seconds from milliseconds plus +5 second buffer. Helper encodes copilot command hooks with timeoutSec clamped to at least 1.
Copilot hook JSON rewriting and versioning
CLI/cmux.swift
Broadens cmux hook JSON rewriting to include .copilotJSON format. Uses JSON-based jsonHookValueContainsCmuxOwnedCommand predicate for ownership checks. Sets "version": 1 for both .flat and .copilotJSON. Updates hook removal to handle .copilotJSON entries.
Copilot-specific permission decision encoding
CLI/cmux.swift, Resources/Localizable.xcstrings
Permission decision encoding branches on source == "copilot": deny payloads include permissionDecisionReason with localized default; allow payloads omit reason. Adds cli.hooks.feed.permissionDeniedReason localization across multiple language codes.
Restrict permission capabilities for Copilot
Sources/Feed/FeedPermissionActionPolicy.swift, CLI/CodexTeamsApprovalBridge.swift
Excludes .copilot source from persistent permission modes and bypass permissions in both policy and bridge, aligning permission-handling with Copilot's approval-gate behavior.
Xcode project wiring for new test file
cmux.xcodeproj/project.pbxproj
Adds CLICopilotHookFeedTests.swift as PBXBuildFile, PBXFileReference, and PBXSourcesBuildPhase entry in cmuxTests target.
Hook install and feed decision integration tests
cmuxTests/CLICopilotHookFeedTests.swift
Install test validates .copilot/hooks/cmux.json layout, version field, required hook keys, and PreToolUse timeout (125 sec). Decision test verifies feed event metadata and allow/deny stdout payloads via Unix socket mock server, including permissionDecisionReason for deny.
Copilot PreToolUse classification unit test
cmuxTests/FeedEventClassificationTests.swift
Adds copilotPreToolUseIsApprovalRequest test asserting Copilot PreToolUse classifies to PermissionRequest for Bash, Read, AskUserQuestion, ExitPlanMode, with actionable == true for side-effecting and read-only tools.
Permission capability test helper and Copilot coverage
cmuxTests/FeedCoordinatorTests.swift
Introduces expectPermissionCapabilities helper to centralize permission-mode assertions. Adds .copilot source coverage and refactors Codex tool-input scenarios to use the new helper.
Documentation and web page updates
docs/agent-hooks.md, docs/feed.md, docs/notifications.md, web/app/[locale]/docs/notifications/page.tsx
All Copilot hooks configuration path references updated from ~/.copilot/config.json to ~/.copilot/hooks/cmux.json.

Sequence Diagram(s)

sequenceDiagram
    participant CopilotCLI as GitHub Copilot CLI
    participant cmuxFeed as cmux hooks feed
    participant FeedEventClassifier
    participant PermissionServer as Permission Server
    
    CopilotCLI->>cmuxFeed: PreToolUse event (toolName, env vars)
    cmuxFeed->>FeedEventClassifier: classify("copilot", "PreToolUse", tool)
    FeedEventClassifier-->>cmuxFeed: PermissionRequest (actionable=true)
    cmuxFeed->>PermissionServer: feed.push {hook_event_name: "PermissionRequest", _source: "copilot"}
    PermissionServer-->>cmuxFeed: {mode: "allow"} or {mode: "deny"}
    alt mode == "deny"
        cmuxFeed-->>CopilotCLI: {permissionDecision: "deny", permissionDecisionReason: "User denied"}
    else mode == "allow"
        cmuxFeed-->>CopilotCLI: {permissionDecision: "allow"}
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • [copilot] PreToolUse feed hook hangs for 120s and never gates tool execution #6574: This PR directly implements the approval gating mechanism described in the issue by registering Copilot PreToolUse in FeedEventClassifier.feedEventSemanticRegistry as approvalRequest, implementing Copilot-specific permission decision response encoding with localized denial reason, updating hook installation to .copilot/hooks/cmux.json, and validating the full flow with integration tests.

Possibly related PRs

  • manaflow-ai/cmux#4225: Both PRs add new agent-specific configuration via the createConfigDirIfMissing and config-dir override machinery in CLI/CMUXCLI+AgentHookDefinitions.swift—Copilot uses .copilot/hooks while Grok uses its own layout.
  • manaflow-ai/cmux#4562: Both PRs add new cmuxTests/*.swift test files and update cmux.xcodeproj/project.pbxproj to wire them into the cmuxTests target's Sources build phase.
  • manaflow-ai/cmux#5010: Both PRs modify CLI/FeedEventClassifier.swift and cmuxTests/FeedEventClassificationTests.swift to adjust per-agent event-to-wire classification semantics via the feedEventSemanticRegistry, ensuring specific events route to actionable approval paths.

Poem

🐇 Hoppity-hop, the hooks found a new nest,
.copilot/hooks/cmux.json — much cleaner address!
PreToolUse now whispers, "May I proceed?"
Allow or deny, approval's guaranteed.
With localized reasons for each denial's grace —
The rabbit secured the Copilot's workspace! 🌙

🚥 Pre-merge checks | ✅ 22 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.92% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (22 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: gate Copilot PreToolUse feed hooks' directly addresses the main objective of the PR: fixing Copilot PreToolUse feed hook gating (issue #6574).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed No Swift 6 actor isolation mistakes introduced. Changes are enum case additions and static method logic updates in utility types (FeedEventClassifier, CodexTeamsApprovalBridge, FeedPermissionAction...
Cmux Swift Blocking Runtime ✅ Passed PR introduces no new blocking runtime primitives in production Swift code. Changes are configuration updates, event classification logic, permission checks, and JSON serialization—all non-blocking....
Cmux Expensive Synchronous Load ✅ Passed PR adds copilot hook support but contains no expensive synchronous agent-history loads (RestorableAgentSessionIndex.load, directory scans, per-record syscalls) on main actor or interactive paths; c...
Cmux Cache Substitution Correctness ✅ Passed This PR contains no cache substitution issues. All data flows read fresh: feed event classification is computed at runtime, feed server decisions are read fresh from responses, and permission capab...
Cmux No Hacky Sleeps ✅ Passed PR introduces no new sleeps, timers, or polling in TypeScript/JavaScript/shell production code. Only changes are 12 Swift files (covered by different rule) and 1 TypeScript documentation file updat...
Cmux Algorithmic Complexity ✅ Passed The PR adds .copilotJSON format support to existing hook rewriting logic. Copilot has a tiny fixed-size collection (up to ~5 hook entries) bounded by agent-defined events, not user data. The recurs...
Cmux Swift Concurrency ✅ Passed PR introduces no legacy async patterns to cmux-owned production Swift code. Only new concurrency pattern is test-only mock socket server using allowed DispatchQueue.global/DispatchSemaphore for con...
Cmux Swift @Concurrent ✅ Passed No Swift concurrent annotation violations found. No async functions added, no invalid @concurrent usage, and properly isolated dispatch queue work in test code.
Cmux Swift File And Package Boundaries ✅ Passed PR makes focused Copilot hook support changes: adds copilotJSON format variant to existing HookFormat enum, updates wire mapping in existing FeedEventClassifier, adds ~27 lines to 34k-line cmux.swi...
Cmux Swiftpm Lockfiles ✅ Passed This PR makes no changes to Package.swift, Package.resolved, .gitignore, or SwiftPM package references—it only modifies source code, tests, localization, documentation, and Xcode project test file...
Cmux Swift Logging ✅ Passed Production Swift code changes comply with logging rules: no print/NSLog/debugPrint/dump in app/runtime code, no Logger isolation violations, no secrets exposed. Test files include no inappropriate...
Cmux User-Facing Error Privacy ✅ Passed All user-facing error messages and documentation in the PR comply with the privacy rules: messages are generic and cmux-centric ("User denied permission via cmux Feed"), no credentials/tokens/env-v...
Cmux Full Internationalization ✅ Passed PR complies with full-internationalization requirements: the new user-facing string cli.hooks.feed.permissionDeniedReason is fully localized (20 locales: ar, bs, da, de, en, es, fr, it, ja, km, k...
Cmux Swiftui State Layout ✅ Passed PR contains no SwiftUI state or layout changes; only CLI hook classification, configuration, and test changes are modified.
Cmux Architecture Rethink ✅ Passed PR adds small correctness fixes to classify Copilot PreToolUse as permission gate with clear ownership (feedEventSemanticRegistry), no timing/dispatch patterns, no new mutable state/observers, and...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed No NSWindow, NSPanel, NSWindowController, SwiftUI Window, or WindowGroup constructs are added or modified. PR modifies only Copilot hook definitions, feed event classification, and test coverage—no...
Cmux Source Artifacts ✅ Passed All changed files are intentional hand-written source code, tests, documentation, or configuration files (Xcode project, localization catalogs). No forbidden artifact directories or auto-generated...
Cmux No Test Or Debug Seam In Production Source ✅ Passed No test/debug seams found in production source file Sources/Feed/FeedPermissionActionPolicy.swift; changes add .copilot to permission-mode checks with no test-only members or guards.
Description check ✅ Passed The PR description comprehensively covers the summary, testing approach, and implementation details, addressing all template requirements.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-6574-copilot-pretooluse-hook

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented Jun 22, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes three root causes that prevented Copilot preToolUse from working as a blocking approval gate: events were not classified as approval requests, resolved decisions were serialized in Claude's hookSpecificOutput format instead of Copilot's top-level permissionDecision schema, and the installed hook targeted the wrong config path without a version key.

  • Classification: FeedEventClassifier now routes Copilot preToolUse for side-effecting tools (bash, edit, create, apply_patch, powershell, str_replace_editor) and interactive tools (ask_user, AskUserQuestion, ExitPlanMode) to PermissionRequest (blocking); read-only tools fall through as telemetry. permissionRequest PascalCase is also retained for already-installed hooks.
  • Decision serialization: renderAgentDecision now emits {"permissionDecision":"allow"} for once-mode approvals and {"permissionDecision":"deny","permissionDecisionReason":"..."} for denials; permissionRequest denials use the existing {"behavior":"deny","message":"..."} shape. Every error and timeout path in the feed hook handler emits a deny via emitCopilotPreToolUseDenyIfNeeded, failing closed.
  • Install path and format: Copilot hooks are now written to ~/.copilot/hooks/cmux.json with "version": 1, using the new copilotJSON format (key bash instead of command, timeoutSec in seconds, optional matcher regex to scope preToolUse to side-effecting tools only). Persistent permission modes and bypass are disabled for Copilot in both the backend policy and the feed-tui UI.

Confidence Score: 5/5

Safe to merge. All three root causes are addressed consistently across the Swift backend, CLI, and feed-tui; the fail-closed deny path is exercised at every timeout and error site; and localization coverage is complete for all 20 catalog locales.

The classification, serialization, and install-path fixes are mutually consistent and covered by new integration tests that run the actual CLI binary and verify the installed JSON, the shell command's fail-closed deny output, and the decision wire format. Permission-mode policy (persistent modes and bypass disabled for Copilot) is applied symmetrically in the Swift backend (FeedPermissionActionPolicy), the CLI bridge (CodexTeamsApprovalBridge), and the feed-tui TypeScript. No actor isolation, blocking runtime, or i18n gaps were found.

No files require special attention.

Important Files Changed

Filename Overview
CLI/FeedEventClassifier.swift Adds Copilot-specific event classification: preToolUse routes side-effecting tools and ask_user/AskUserQuestion/ExitPlanMode to PermissionRequest (blocking), and read-only tools to PreToolUse (telemetry). Also exposes the copilotPreToolUseApprovalToolMatcher for use as a Copilot hook matcher field.
CLI/cmux.swift Adds Copilot-specific decision serialization in renderAgentDecision (top-level `permissionDecision: allow
CLI/CMUXCLI+AgentHookDefinitions.swift Migrates Copilot from nested to new copilotJSON hook format, updates event names to camelCase, changes install path to ~/.copilot/hooks/cmux.json with version: 1, and adds fail-closed copilotPreToolUseAgentHookShellCommand that denies when cmux is unavailable.
CLI/CodexTeamsApprovalBridge.swift Adds copilot to the exclusion lists for persistent permission modes and bypass permissions, matching the backend policy changes in FeedPermissionActionPolicy.
Sources/Feed/FeedPermissionActionPolicy.swift Excludes copilot from supportsPersistentPermissionModes and supportsBypassPermissions, disabling "always", "all", and "bypass" modes in the backend, consistent with the UI changes.
Resources/feed-tui/index.ts Extracts sourceSupportsPersistentPermissionModes and updates sourceSupportsAlwaysPermissionMode and sourceSupportsAllPermissionMode to use it, gating these modes for copilot and hermes-agent consistently. No runtime-no-hacky-sleeps concerns.
Resources/Localizable.xcstrings Adds cli.hooks.feed.permissionDeniedReason with translations for all 20 locales supported by the catalog (ar, bs, da, de, en, es, fr, it, ja, km, ko, nb, pl, pt-BR, ru, th, tr, uk, zh-Hans, zh-Hant). Full i18n coverage.
cmuxTests/CLICopilotHookFeedTests.swift New 682-line test suite covering Copilot hook install path/version, stale hook cleanup, cmux-unavailable fail-closed deny output, and Feed decision serialization for deny/once/always/permissionRequest modes. Uses Swift Testing framework.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant C as Copilot Agent
    participant H as Shell Hook
    participant CLI as cmux CLI hooks feed
    participant FC as FeedEventClassifier
    participant F as Feed Socket
    participant U as User (Feed TUI)

    C->>H: "preToolUse {toolName: bash, ...}"
    Note over H: matcher filters to side-effecting tools only
    H->>CLI: cmux hooks feed --source copilot --event preToolUse
    CLI->>FC: classify(copilot, preToolUse, bash)
    FC-->>CLI: "PermissionRequest, isActionable=true"
    CLI->>F: Send approval request (block 120s)
    F->>U: Show approval card
    alt User approves (Once)
        U-->>F: "decision: kind=permission, mode=once"
        F-->>CLI: result
        CLI->>H: "stdout: permissionDecision=allow"
        H-->>C: allow tool
    else User denies
        U-->>F: "decision: kind=permission, mode=deny"
        F-->>CLI: result
        CLI->>H: "stdout: permissionDecision=deny"
        H-->>C: block tool
    else Timeout or cmux unavailable or no Feed socket
        CLI->>H: "stdout: permissionDecision=deny (fail-closed)"
        H-->>C: block tool
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant C as Copilot Agent
    participant H as Shell Hook
    participant CLI as cmux CLI hooks feed
    participant FC as FeedEventClassifier
    participant F as Feed Socket
    participant U as User (Feed TUI)

    C->>H: "preToolUse {toolName: bash, ...}"
    Note over H: matcher filters to side-effecting tools only
    H->>CLI: cmux hooks feed --source copilot --event preToolUse
    CLI->>FC: classify(copilot, preToolUse, bash)
    FC-->>CLI: "PermissionRequest, isActionable=true"
    CLI->>F: Send approval request (block 120s)
    F->>U: Show approval card
    alt User approves (Once)
        U-->>F: "decision: kind=permission, mode=once"
        F-->>CLI: result
        CLI->>H: "stdout: permissionDecision=allow"
        H-->>C: allow tool
    else User denies
        U-->>F: "decision: kind=permission, mode=deny"
        F-->>CLI: result
        CLI->>H: "stdout: permissionDecision=deny"
        H-->>C: block tool
    else Timeout or cmux unavailable or no Feed socket
        CLI->>H: "stdout: permissionDecision=deny (fail-closed)"
        H-->>C: block tool
    end
Loading

Reviews (36): Last reviewed commit: "test: avoid suite captures in migrated h..." | Re-trigger Greptile

Comment thread CLI/cmux.swift Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
docs/notifications.md (1)

120-155: ⚠️ Potential issue | 🟡 Minor

Add the required version field to the Copilot CLI hooks configuration.

The JSON example is missing the top-level version: 1 field required by Copilot CLI. Without it, the configuration will fail to load.

Proposed fix
 {
+  "version": 1,
   "hooks": {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/notifications.md` around lines 120 - 155, The JSON example in the
cmux.json hooks configuration is missing the required top-level version field
that Copilot CLI needs to properly parse the configuration. Add a version field
set to 1 at the root level of the JSON object, before the hooks object, so the
complete structure includes both version and hooks properties at the top level.
web/app/[locale]/docs/notifications/page.tsx (1)

269-300: ⚠️ Potential issue | 🟡 Minor

Add the required top-level version field.

The rendered hook example at lines 269–300 is missing version: 1, while the .github/hooks/notify.json example immediately below (lines 302–308) correctly includes it. Users copying the cmux.json example will get an invalid hook file that Copilot CLI will reject. GitHub's official hook schema requires version: 1 as a top-level field.

🛠️ Proposed fix
-      <CodeBlock title="~/.copilot/hooks/cmux.json" lang="json">{`{
+      <CodeBlock title="~/.copilot/hooks/cmux.json" lang="json">{`{
+  "version": 1,
   "hooks": {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/app/`[locale]/docs/notifications/page.tsx around lines 269 - 300, The
CodeBlock component displaying the cmux.json hook example is missing the
required top-level version field that the hook schema mandates. Add "version": 1
as the first property in the JSON object within the CodeBlock template string,
positioning it before the "hooks" property. This will make the example valid and
consistent with the notify.json example shown immediately below it.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@docs/notifications.md`:
- Around line 120-155: The JSON example in the cmux.json hooks configuration is
missing the required top-level version field that Copilot CLI needs to properly
parse the configuration. Add a version field set to 1 at the root level of the
JSON object, before the hooks object, so the complete structure includes both
version and hooks properties at the top level.

In `@web/app/`[locale]/docs/notifications/page.tsx:
- Around line 269-300: The CodeBlock component displaying the cmux.json hook
example is missing the required top-level version field that the hook schema
mandates. Add "version": 1 as the first property in the JSON object within the
CodeBlock template string, positioning it before the "hooks" property. This will
make the example valid and consistent with the notify.json example shown
immediately below it.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 52b2a30c-fb94-485b-8ec7-83f5754e1204

📥 Commits

Reviewing files that changed from the base of the PR and between 165af15 and 110987f.

📒 Files selected for processing (9)
  • CLI/CMUXCLI+AgentHookDefinitions.swift
  • CLI/FeedEventClassifier.swift
  • CLI/cmux.swift
  • cmuxTests/CLIGenericHookPersistenceTests.swift
  • cmuxTests/FeedEventClassificationTests.swift
  • docs/agent-hooks.md
  • docs/feed.md
  • docs/notifications.md
  • web/app/[locale]/docs/notifications/page.tsx

@austinywang
austinywang force-pushed the issue-6574-copilot-pretooluse-hook branch from 110987f to 899cca3 Compare June 22, 2026 08:09

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CLI/cmux.swift`:
- Line 28237: Combine the two separate if statements that both assign the same
value to existing["version"] into a single conditional statement using a logical
OR operator. Replace the semicolon-separated statements with a single if
statement that checks both conditions: if the def.format is .flat OR if
def.requiresVersionKey is true, then set existing["version"] = 1. This improves
readability and follows Swift style conventions by avoiding semicolon-separated
statements on a single line.
- Line 33245: The single-line conditional in the source == "copilot" check
exceeds 280 characters and contains nested ternary operators, dictionary
literals, and localized string initialization that reduce readability. Refactor
this by extracting intermediate variables: first create a variable for the
permission decision based on the mode == "deny" condition, then construct the
result dictionary separately (including the localized string for
permissionDecisionReason when mode is "deny"), and finally pass the dictionary
to the encode function. This will break the logic across multiple lines and
significantly improve maintainability without changing the behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5ce88d3a-03ce-49fa-98eb-f1545ffa4ae6

📥 Commits

Reviewing files that changed from the base of the PR and between 899cca3 and f280731.

📒 Files selected for processing (6)
  • CLI/CMUXCLI+AgentHookDefinitions.swift
  • CLI/cmux.swift
  • Resources/Localizable.xcstrings
  • cmux.xcodeproj/project.pbxproj
  • docs/notifications.md
  • web/app/[locale]/docs/notifications/page.tsx

Comment thread CLI/cmux.swift Outdated
Comment thread CLI/cmux.swift Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cmuxTests/CLICopilotHookFeedTests.swift (1)

136-136: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Hardcoded English string creates locale-dependent test.

This test assumes the CLI runs in English locale and compares against the hardcoded English denial reason. If the test environment's locale differs, the assertion will fail even though the behavior is correct.

Consider one of these approaches:

  1. Load the localized string dynamically from the cli.hooks.feed.permissionDeniedReason key for comparison
  2. Assert that permissionDecisionReason is non-empty rather than checking exact text
  3. Document that the test requires English locale
💡 Example: non-empty assertion approach
-    XCTAssertEqual(denyOutput["permissionDecisionReason"] as? String, "User denied permission via cmux Feed.")
+    XCTAssertFalse((denyOutput["permissionDecisionReason"] as? String ?? "").isEmpty, "Expected non-empty denial reason")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmuxTests/CLICopilotHookFeedTests.swift` at line 136, The XCTAssertEqual
assertion in CLICopilotHookFeedTests.swift hardcodes an English string "User
denied permission via cmux Feed." which causes test failures in non-English
locales. Replace this exact string comparison with either a dynamic lookup of
the localized string from the cli.hooks.feed.permissionDeniedReason key, or
change the assertion to simply verify that permissionDecisionReason is non-empty
instead of matching specific text, ensuring the test passes regardless of the
system locale.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CLI/FeedEventClassifier.swift`:
- Around line 112-114: Add a comment above the if statement checking for source
== "copilot" that explains why Copilot events require the special case. The
comment should clarify that Copilot approvals must use the generic
"PermissionRequest" wire event (rather than tool-specific events) because
Copilot's permission response format uses a top-level permissionDecision JSON
structure instead of tool-specific decision types.

In `@cmuxTests/FeedEventClassificationTests.swift`:
- Around line 104-105: The test assertions for AskUserQuestion and ExitPlanMode
tools in the classify function calls are incomplete. Currently, lines 104-105
only verify the name property matches "PermissionRequest", but they should also
verify that actionable is true, consistent with the assertions for Bash and Read
tools on lines 100-103. Update both classify calls for AskUserQuestion and
ExitPlanMode to include checks that the actionable property equals true,
mirroring the pattern used for the other tool types.

---

Outside diff comments:
In `@cmuxTests/CLICopilotHookFeedTests.swift`:
- Line 136: The XCTAssertEqual assertion in CLICopilotHookFeedTests.swift
hardcodes an English string "User denied permission via cmux Feed." which causes
test failures in non-English locales. Replace this exact string comparison with
either a dynamic lookup of the localized string from the
cli.hooks.feed.permissionDeniedReason key, or change the assertion to simply
verify that permissionDecisionReason is non-empty instead of matching specific
text, ensuring the test passes regardless of the system locale.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: dbde51b9-9e46-411a-a8a7-5c36f49dc8be

📥 Commits

Reviewing files that changed from the base of the PR and between f280731 and bba9f38.

📒 Files selected for processing (6)
  • CLI/CMUXCLI+AgentHookDefinitions.swift
  • CLI/FeedEventClassifier.swift
  • CLI/cmux.swift
  • Resources/Localizable.xcstrings
  • cmuxTests/CLICopilotHookFeedTests.swift
  • cmuxTests/FeedEventClassificationTests.swift

Comment thread CLI/FeedEventClassifier.swift
Comment thread cmuxTests/FeedEventClassificationTests.swift Outdated
@stumash

stumash commented Jul 1, 2026

Copy link
Copy Markdown

@austinywang is this abandoned? can I re-open my other PR #6503 then so we can merge something in that enables hooks for Github Copilot users with a short-term fix?

@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in cmux backlog Sep 23, 2026

This branch was successfully deployed

1 active deployment
Preview – cmux — cb1ea2d6 Deployed Jun 22, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[copilot] PreToolUse feed hook hangs for 120s and never gates tool execution

4 participants