Commit ae6549431624

Vincent Demeester <vincent@sbr.pm>
2026-02-02 09:58:39
feat(git): add GitHub Copilot hooks with auto-install via git template
Port Claude Code hooks to work with GitHub Copilot CLI: - Add copilot-to-claude.sh wrapper to translate JSON formats - Add claude-hooks.json with preToolUse/postToolUse hooks - Add post-checkout hook in git template to auto-install on clone - Configure init.templateDir and core.excludesFile in git.nix - Add .github/hooks to global gitignore (auto-installed, never commit) Hooks ported: sessionStart, sessionEnd, preToolUse, postToolUse Not available in Copilot: Stop, PermissionRequest Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 2dfed1a
Changed files (5)
dots
home
common
shell
dots/.config/copilot-hooks/claude-hooks.json
@@ -0,0 +1,38 @@
+{
+  "version": 1,
+  "hooks": {
+    "sessionStart": [
+      {
+        "type": "command",
+        "bash": "claude-hooks-initialize-session",
+        "timeoutSec": 5
+      }
+    ],
+    "sessionEnd": [
+      {
+        "type": "command",
+        "bash": "claude-hooks-save-session",
+        "timeoutSec": 10
+      }
+    ],
+    "preToolUse": [
+      {
+        "type": "command",
+        "bash": ".github/hooks/copilot-to-claude.sh claude-hooks-validate-git-push",
+        "timeoutSec": 5
+      }
+    ],
+    "postToolUse": [
+      {
+        "type": "command",
+        "bash": ".github/hooks/copilot-to-claude.sh claude-hooks-capture-tool-output",
+        "timeoutSec": 5
+      },
+      {
+        "type": "command",
+        "bash": ".github/hooks/copilot-to-claude.sh claude-hooks-update-terminal-title",
+        "timeoutSec": 2
+      }
+    ]
+  }
+}
dots/.config/copilot-hooks/copilot-to-claude.sh
@@ -0,0 +1,45 @@
+#!/usr/bin/env bash
+# Wrapper script to translate Copilot hook JSON format to Claude Code format
+# Usage: copilot-to-claude.sh <claude-hooks-binary>
+#
+# Copilot format:
+#   { "toolName": "bash", "toolArgs": "{\"command\":\"...\"}", "toolResult": {...} }
+#
+# Claude Code format:
+#   { "tool_name": "Bash", "tool_input": {"command": "..."}, "tool_response": {...} }
+
+set -euo pipefail
+
+BINARY="${1:-}"
+if [[ -z "$BINARY" ]]; then
+    echo "Usage: $0 <claude-hooks-binary>" >&2
+    exit 1
+fi
+
+# Read stdin
+INPUT=$(cat)
+
+# Check if we have input
+if [[ -z "$INPUT" ]]; then
+    exit 0
+fi
+
+# Transform Copilot format to Claude Code format using jq
+# - toolName -> tool_name (capitalize first letter for Claude Code convention)
+# - toolArgs (string) -> tool_input (parsed object)
+# - toolResult -> tool_response
+TRANSFORMED=$(echo "$INPUT" | jq -c '
+{
+  tool_name: (.toolName | split("") | .[0:1] | map(ascii_upcase) | join("") + (.toolName | .[1:])),
+  tool_input: (if .toolArgs then (.toolArgs | fromjson) else {} end),
+  tool_response: (.toolResult // {}),
+  conversation_id: (.sessionId // "copilot")
+}
+' 2>/dev/null) || {
+    # If jq fails, just pass through empty and let binary handle it
+    exit 0
+}
+
+# Call the Claude hooks binary with transformed input
+echo "$TRANSFORMED" | "$BINARY"
+exit $?
dots/.config/git/template/hooks/post-checkout
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+# Git post-checkout hook (installed via git template)
+# Automatically installs GitHub Copilot hooks to .github/hooks/
+#
+# This runs after:
+# - git clone (prev_head is null-ref)
+# - git checkout/switch (branch checkout when flag=1)
+
+set -euo pipefail
+
+COPILOT_HOOKS_SOURCE="${XDG_CONFIG_HOME:-$HOME/.config}/copilot-hooks"
+
+# Only proceed if we have hooks to install
+if [[ ! -d "$COPILOT_HOOKS_SOURCE" ]]; then
+    exit 0
+fi
+
+# Only install if .github/hooks doesn't exist yet
+if [[ -d ".github/hooks" ]]; then
+    exit 0
+fi
+
+# Check if this looks like a repo that might use Copilot
+# (has .github/ directory or is a code project)
+if [[ ! -d ".github" ]] && [[ ! -f "package.json" ]] && [[ ! -f "go.mod" ]] && [[ ! -f "Cargo.toml" ]] && [[ ! -f "flake.nix" ]] && [[ ! -f "pyproject.toml" ]]; then
+    exit 0
+fi
+
+# Install hooks
+mkdir -p .github/hooks
+cp -r "$COPILOT_HOOKS_SOURCE"/* .github/hooks/ 2>/dev/null || true
+
+# Make scripts executable
+chmod +x .github/hooks/*.sh 2>/dev/null || true
+
+echo "Installed Copilot hooks to .github/hooks/"
dots/Makefile
@@ -53,6 +53,10 @@ lazypr : ~/.config/lazypr/config.toml
 all += gh-news
 gh-news : ~/.config/gh-news/config.toml
 
+all += git-template copilot-hooks
+git-template : ~/.config/git/template
+copilot-hooks : ~/.config/copilot-hooks
+
 # Backward compatibility: symlink ~/.claude to ~/.config/claude
 ~/.claude : force
 	@echo "๐Ÿ”— Creating backward compatibility symlink: ~/.claude -> ~/.config/claude"
home/common/shell/git.nix
@@ -103,6 +103,8 @@ in
       core = {
         pager = "${pkgs.delta}/bin/delta";
         abbrev = 12;
+        # Override system excludesFile to use XDG user ignores
+        excludesFile = "${config.xdg.configHome}/git/ignore";
         # pager = "${pkgs.delta}/bin/delta --syntax-theme GitHub";
         # editor = "${pkgs.emacs}/bin/emacsclient -t";
       };
@@ -121,6 +123,7 @@ in
       };
       init = {
         defaultBranch = "main";
+        templateDir = "${config.xdg.configHome}/git/template";
       };
       color = {
         status = "auto";
@@ -253,6 +256,8 @@ in
       "#*#"
       ".makefile"
       ".clean"
+      # Copilot hooks are auto-installed via git template, never commit
+      ".github/hooks"
     ];
   };
 }