From d298ac93246b63b5473cacb396e082c4700d3062 Mon Sep 17 00:00:00 2001 From: Corwin Marsh Date: Wed, 23 Sep 2026 09:33:58 -0700 Subject: [PATCH 1/2] feat: opt in Copilot hooks through tmux auto-install --- README.md | 12 ++++++------ coding-agents-tmux.tmux | 18 +++++++++++++++++- test/tmux-plugin-rename.test.ts | 21 +++++++++++++++++++-- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 2e09f5d..03963ce 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Recommended settings: ```tmux set -g @coding-agents-tmux-provider 'plugin' -set -g @coding-agents-tmux-auto-install 'opencode,pi,codex,claude' +set -g @coding-agents-tmux-auto-install 'opencode,pi,codex,claude,copilot' set -g @coding-agents-tmux-menu-key 'O' set -g @coding-agents-tmux-popup-key 'P' set -g @coding-agents-tmux-waiting-menu-key 'W' @@ -125,7 +125,7 @@ It also installs or updates Codex hook integration under: ~/.codex/hooks.json ``` -With the recommended `@coding-agents-tmux-auto-install 'opencode,pi,codex,claude'` setting, it also installs or updates Claude Code hook integration under: +With the recommended `@coding-agents-tmux-auto-install 'opencode,pi,codex,claude,copilot'` setting, it also installs or updates Claude Code hook integration under: ```text ~/.claude/settings.json @@ -160,10 +160,10 @@ You can also control all tmux-managed installs together: ```tmux set -g @coding-agents-tmux-auto-install 'auto' set -g @coding-agents-tmux-auto-install 'off' -set -g @coding-agents-tmux-auto-install 'opencode,pi,codex,claude' +set -g @coding-agents-tmux-auto-install 'opencode,pi,codex,claude,copilot' ``` -The explicit `opencode,pi,codex,claude` list is the recommended README setting because it makes the intended managed installs obvious in your tmux config. When `@coding-agents-tmux-auto-install` is set, it takes precedence over the individual install toggles. +The explicit `opencode,pi,codex,claude,copilot` list is the recommended README setting because it makes the intended managed installs obvious in your tmux config. When `@coding-agents-tmux-auto-install` is set, it takes precedence over the individual install toggles. Kiro CLI has no hook/extension installer; it uses pane and preview detection without an install step. ## Usage @@ -468,7 +468,7 @@ set -g @coding-agents-tmux-install-claude-hooks 'on' or the shared selector: ```tmux -set -g @coding-agents-tmux-auto-install 'opencode,pi,codex,claude' +set -g @coding-agents-tmux-auto-install 'opencode,pi,codex,claude,copilot' ``` 3. Optionally inspect the managed hook template before merging it into project or user Claude settings: @@ -498,7 +498,7 @@ This means any `kiro-cli` pane can be discovered and switched to without naming An interactive `copilot` process (or `copilot.exe` on Windows) in a tmux pane is discovered in `list`, the chooser/popup, and `status`. The `gh copilot` launcher is also discovered when its foreground process group contains a direct Copilot CLI child (or a known npm Node wrapper with a Copilot child); GitHub CLI's downloaded copy does not need a separate standalone installation. Use `--agent copilot` to filter navigation. No hooks are required: without them, activity and status remain `unknown` rather than guessing a wait. Title text, unrelated `gh` commands, and arbitrary `copilot-*` commands are not detected. The npm-installed CLI's Node wrapper is recognized by its executable path; generic wrapper support is tracked separately in #19. -For optional, **local Copilot CLI only** turn and prompt state, preview the configuration with `coding-agents-tmux copilot-hooks-template`, then explicitly run `coding-agents-tmux install-copilot`. This creates only `${COPILOT_HOME:-~/.copilot}/hooks/coding-agents-tmux.json`; other user hooks are untouched. Restart existing Copilot CLI sessions so hooks load. Do not put these hooks in `.github/hooks/` (repository hooks also execute in Copilot cloud agent). The tmux plugin never auto-installs Copilot hooks. +For optional, **local Copilot CLI only** turn and prompt state, add `copilot` to `@coding-agents-tmux-auto-install` (or use `auto`) to install/update the user-level hooks whenever the tmux plugin loads. You can preview the configuration with `coding-agents-tmux copilot-hooks-template` or install it directly with `coding-agents-tmux install-copilot`. This creates only `${COPILOT_HOME:-~/.copilot}/hooks/coding-agents-tmux.json`; other user hooks are untouched. Restart existing Copilot CLI sessions so hooks load. Do not put these hooks in `.github/hooks/` (repository hooks also execute in Copilot cloud agent). Leaving `copilot` out of the selector does not install hooks. The adapter correlates `TMUX_PANE`, the hook process's Copilot parent PID, and the Copilot `sessionId`, including concurrent sessions in one directory and `/clear` (which ends the old session and starts another). An exited or backgrounded Copilot process invalidates its hook state even if another CLI starts in the same pane. `userPromptSubmitted` means running; `agentStop` means idle. Only `notification` types `permission_prompt` and `elicitation_dialog` assert waiting-question and waiting-input respectively; automatic permission checks and `preToolUse` never assert a wait. An elicitation notification does not reveal whether its form contains multiple choices, so it is conservatively labeled input. Resolved prompts clear at the next turn boundary (`agentStop`); hooks do not expose an immediate resolution event, so the state can briefly lag after an answer. Hook state expires after 60 seconds without a subsequent event (including an unanswered prompt), reverting to the coarse navigable state instead of retaining a false wait. `inspect` reports hook source, age and expiry or command-only fallback. If hooks are disabled, folder trust blocks them, tmux pane identity is missing, or a hook is missed, no directory-only match is made. `COPILOT_HOME` selects the hook directory; `CODING_AGENTS_TMUX_COPILOT_STATE_DIR` overrides the local state directory. The integration neither grants permissions nor injects context. diff --git a/coding-agents-tmux.tmux b/coding-agents-tmux.tmux index 83e99d1..329456d 100755 --- a/coding-agents-tmux.tmux +++ b/coding-agents-tmux.tmux @@ -365,6 +365,12 @@ install_claude_hooks() { fi } +install_copilot_hooks() { + if ! "$CURRENT_DIR/bin/coding-agents-tmux" install-copilot >/dev/null 2>&1; then + tmux display-message "coding-agents-tmux: failed to install Copilot CLI hook configuration" + fi +} + install_pi_extension() { local extension_source pi_dir extension_dir extension_target existing_target installed_changed @@ -395,7 +401,7 @@ install_pi_extension() { } main() { - local menu_key popup_key waiting_menu_key waiting_popup_key provider server_map popup_filter popup_width popup_height popup_title status_enabled status_style status_position status_option status_interval status_mode install_plugin install_codex install_pi install_claude auto_install_value status_text_segment status_inline_segment status_tone_segment status_refresh_command + local menu_key popup_key waiting_menu_key waiting_popup_key provider server_map popup_filter popup_width popup_height popup_title status_enabled status_style status_position status_option status_interval status_mode install_plugin install_codex install_pi install_claude install_copilot auto_install_value status_text_segment status_inline_segment status_tone_segment status_refresh_command local status_prefix status_color_neutral status_color_busy status_color_waiting status_color_idle status_color_unknown notify_command local previous_status_segment previous_status_option previous_menu_key previous_popup_key previous_waiting_menu_key previous_waiting_popup_key menu_key="$(normalize_binding_key "$(get_tmux_option '@coding-agents-tmux-menu-key' 'O')")" @@ -418,18 +424,21 @@ main() { install_codex='on' install_pi='on' install_claude='on' + install_copilot='on' ;; off|"") install_plugin='off' install_codex='off' install_pi='off' install_claude='off' + install_copilot='off' ;; *) install_plugin='off' install_codex='off' install_pi='off' install_claude='off' + install_copilot='off' if auto_install_includes "$auto_install_value" 'opencode'; then install_plugin='on' @@ -446,6 +455,9 @@ main() { if auto_install_includes "$auto_install_value" 'claude'; then install_claude='on' fi + if auto_install_includes "$auto_install_value" 'copilot'; then + install_copilot='on' + fi ;; esac else @@ -453,6 +465,7 @@ main() { install_codex="$(normalize_toggle "$(get_tmux_option '@coding-agents-tmux-install-codex-hooks' 'on')")" install_pi="$(normalize_toggle "$(get_tmux_option '@coding-agents-tmux-install-pi-extension' 'on')")" install_claude="$(normalize_toggle "$(get_tmux_option '@coding-agents-tmux-install-claude-hooks' 'off')")" + install_copilot='off' fi status_enabled="$(get_tmux_option '@coding-agents-tmux-status' 'on')" status_style="$(get_tmux_option '@coding-agents-tmux-status-style' 'tmux')" @@ -497,6 +510,9 @@ main() { if [ "$install_claude" = "on" ]; then install_claude_hooks fi + if [ "$install_copilot" = "on" ]; then + install_copilot_hooks + fi local popup_filter_arg="" case "$popup_filter" in diff --git a/test/tmux-plugin-rename.test.ts b/test/tmux-plugin-rename.test.ts index 8dd091d..3a31cb1 100644 --- a/test/tmux-plugin-rename.test.ts +++ b/test/tmux-plugin-rename.test.ts @@ -6,6 +6,7 @@ import { mkdtempSync, readlinkSync, readFileSync, + rmSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; @@ -304,7 +305,7 @@ exit 1 } }); -test("coding-agents-tmux.tmux honors @coding-agents-tmux-auto-install lists including claude", async () => { +test("coding-agents-tmux.tmux honors @coding-agents-tmux-auto-install lists including claude and copilot", async () => { const fakeTmux = installFakeTmux(` log_path='__LOG_PATH__' option="\${!#}" @@ -319,7 +320,7 @@ show-option) printf 'off\n' ;; @coding-agents-tmux-auto-install) - printf 'pi,claude\n' + printf '%s\n' "\${AUTO_INSTALL_SELECTION:-pi,claude,copilot}" ;; esac exit 0 @@ -338,6 +339,7 @@ exit 1 const piHome = join(home, ".pi-home"); const codexHome = join(home, ".codex-home"); const claudeHome = join(home, ".claude-home"); + const copilotHome = join(home, ".copilot-home"); installFakeNpm(fakeTmux.pathEntry); const restoreEnv = setEnv({ HOME: home, @@ -346,6 +348,8 @@ exit 1 PI_CODING_AGENT_DIR: piHome, CODEX_HOME: codexHome, CLAUDE_HOME: claudeHome, + COPILOT_HOME: copilotHome, + AUTO_INSTALL_SELECTION: "pi,claude", }); try { @@ -353,15 +357,28 @@ exit 1 const newPluginPath = join(configHome, "opencode", "plugins", "coding-agents-tmux.ts"); const newPiExtensionPath = join(piHome, "extensions", "coding-agents-tmux", "index.ts"); const claudeSettingsPath = join(claudeHome, "settings.json"); + const copilotHooksPath = join(copilotHome, "hooks", "coding-agents-tmux.json"); const codexHooksPath = join(codexHome, "hooks.json"); assert.equal(result.exitCode, 0); assert.equal(result.stderrText.trim(), ""); + assert.equal(existsSync(copilotHooksPath), false); + process.env.AUTO_INSTALL_SELECTION = "pi,claude,copilot"; + const optedIn = await runCommand([join(process.cwd(), "coding-agents-tmux.tmux")]); + assert.equal(optedIn.exitCode, 0); + assert.equal(optedIn.stderrText.trim(), ""); assert.equal(existsSync(newPluginPath), false); assert.ok(existsSync(newPiExtensionPath)); assert.ok(existsSync(claudeSettingsPath)); + assert.ok(existsSync(copilotHooksPath)); assert.equal(existsSync(codexHooksPath), false); assert.match(readFileSync(claudeSettingsPath, "utf8"), /claude-hook-state/); + assert.match(readFileSync(copilotHooksPath, "utf8"), /copilot-hook-state/); + rmSync(copilotHooksPath); + process.env.AUTO_INSTALL_SELECTION = "auto"; + const all = await runCommand([join(process.cwd(), "coding-agents-tmux.tmux")]); + assert.equal(all.exitCode, 0); + assert.ok(existsSync(copilotHooksPath)); } finally { restoreEnv(); } From ffa7ea695f51d448ebf259c5d5be5b2e599806c8 Mon Sep 17 00:00:00 2001 From: Corwin Marsh Date: Wed, 23 Sep 2026 09:41:46 -0700 Subject: [PATCH 2/2] docs: clarify Copilot hook install help --- src/cli.ts | 4 +++- test/cli.test.ts | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/cli.ts b/src/cli.ts index eaa0eec..c18249f 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -1038,7 +1038,9 @@ async function main(): Promise { program .command("install-copilot") - .description("Explicitly install Copilot CLI user-level hooks; never run automatically") + .description( + "Install Copilot CLI user-level hooks directly or through opt-in tmux auto-install", + ) .action(runInstallCopilotCommand); program diff --git a/test/cli.test.ts b/test/cli.test.ts index 4124f6d..b68ba24 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -412,6 +412,10 @@ test("CLI help and tmux-config work through the entrypoint script", async () => assert.equal(helpResult.exitCode, 0); assert.match(helpResult.stdoutText, /Usage: coding-agents-tmux/); assert.match(helpResult.stdoutText, /tmux-config/); + const copilotHelp = await runCommand([BIN_PATH, "install-copilot", "--help"]); + assert.equal(copilotHelp.exitCode, 0); + assert.match(copilotHelp.stdoutText, /opt-in tmux\s+auto-install/); + assert.doesNotMatch(copilotHelp.stdoutText, /never run automatically/); assert.equal(configResult.exitCode, 0); assert.match(configResult.stdoutText, /# >>> coding-agents-tmux >>>/); assert.match(configResult.stdoutText, /--provider/);