Skip to content

Commit 76d4221

Browse files
authored
fix(nix): add optional Claude Code hook setup (#480)
1 parent 0f098ae commit 76d4221

3 files changed

Lines changed: 137 additions & 5 deletions

File tree

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ in {
133133
programs.peon-ping = {
134134
enable = true;
135135
package = inputs.peon-ping.packages.${pkgs.system}.default;
136+
claudeCodeIntegration = true;
136137
137138
settings = {
138139
default_pack = "glados";
@@ -169,7 +170,7 @@ in {
169170
enableZshIntegration = true;
170171
};
171172
172-
# Cursor hooks
173+
# Optional extra IDE hooks, like Cursor
173174
home.file.".cursor/hooks.json".text = builtins.toJSON {
174175
version = 1;
175176
hooks = {
@@ -197,13 +198,13 @@ For packs listed on [openpeon.com](https://openpeon.com/), find the GitHub repos
197198
}
198199
```
199200

200-
**IDE hooks**: peon-ping Home Manager module will not setup your IDE hooks to avoid conflicting updates. You must define these hooks yourself (see example above) depending on how you usually manage your IDE configuration.
201-
- peon-ping provide adapters scripts for various IDE such as `cursor.sh` - see [`adapters/`](https://github.com/PeonPing/peon-ping/tree/main/adapters)
202-
- You need to call them as your hook such command like
201+
**Claude Code hooks**: set `programs.peon-ping.claudeCodeIntegration = true;` to install the Claude Code hook scripts under `~/.claude/hooks/peon-ping/` and merge the standard peon-ping hook entries into `~/.claude/settings.json`.
202+
203+
**Other IDE hooks**: adapters for other IDEs are still opt-in so the module does not overwrite unrelated IDE settings. peon-ping provides adapter scripts such as `cursor.sh` in [`adapters/`](https://github.com/PeonPing/peon-ping/tree/main/adapters), and you can wire them like this:
203204
```sh
204205
${inputs.peon-ping.packages.${pkgs.system}.default}/share/peon-ping/adapters/$YOUR_IDE.sh EVENT_NAME
205206
```
206-
See Cursor example above
207+
See the Cursor example above.
207208

208209
## What you'll hear
209210

nix/hm-module.nix

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ with lib;
55
let
66
cfg = config.programs.peon-ping;
77
jsonFormat = pkgs.formats.json { };
8+
claudeHooksJsonFormat = pkgs.formats.json { };
89

910
ogPacksVersion = "1.4.0";
1011

@@ -118,6 +119,15 @@ in
118119
Whether to enable Bash completions and alias.
119120
'';
120121
};
122+
123+
claudeCodeIntegration = mkOption {
124+
type = types.bool;
125+
default = false;
126+
description = ''
127+
Whether to install Claude Code hook files under ~/.claude/hooks/peon-ping
128+
and merge peon-ping hook registrations into ~/.claude/settings.json.
129+
'';
130+
};
121131
};
122132

123133
config = mkIf cfg.enable {
@@ -141,6 +151,110 @@ in
141151
# Overrides any config.json that may be present in the package.
142152
home.file.".openpeon/config.json".source = jsonFormat.generate "peon-ping-config" cfg.settings;
143153

154+
home.file = mkIf cfg.claudeCodeIntegration {
155+
".claude/hooks/peon-ping/peon.sh".source = "${cfg.package}/bin/peon";
156+
".claude/hooks/peon-ping/scripts/hook-handle-use.sh".source = "${cfg.package}/share/peon-ping/scripts/hook-handle-use.sh";
157+
".claude/hooks/peon-ping/scripts/hook-handle-rename.sh".source = "${cfg.package}/share/peon-ping/scripts/hook-handle-rename.sh";
158+
};
159+
160+
home.activation.peonPingClaudeCodeHooks = mkIf cfg.claudeCodeIntegration (lib.hm.dag.entryAfter [ "writeBoundary" ] ''
161+
hooks_json='${claudeHooksJsonFormat.generate "peon-claude-hooks" {
162+
hooks = {
163+
SessionStart = [{
164+
matcher = "";
165+
hooks = [{
166+
type = "command";
167+
command = "${config.home.homeDirectory}/.claude/hooks/peon-ping/peon.sh";
168+
timeout = 10;
169+
}];
170+
}];
171+
SessionEnd = [{
172+
matcher = "";
173+
hooks = [{ type = "command"; command = "${config.home.homeDirectory}/.claude/hooks/peon-ping/peon.sh"; timeout = 10; async = true; }];
174+
}];
175+
SubagentStart = [{
176+
matcher = "";
177+
hooks = [{ type = "command"; command = "${config.home.homeDirectory}/.claude/hooks/peon-ping/peon.sh"; timeout = 10; async = true; }];
178+
}];
179+
UserPromptSubmit = [
180+
{
181+
matcher = "";
182+
hooks = [{ type = "command"; command = "${config.home.homeDirectory}/.claude/hooks/peon-ping/peon.sh"; timeout = 10; async = true; }];
183+
}
184+
{
185+
matcher = "";
186+
hooks = [
187+
{ type = "command"; command = "${config.home.homeDirectory}/.claude/hooks/peon-ping/scripts/hook-handle-use.sh"; timeout = 5; }
188+
{ type = "command"; command = "${config.home.homeDirectory}/.claude/hooks/peon-ping/scripts/hook-handle-rename.sh"; timeout = 5; }
189+
];
190+
}
191+
];
192+
Stop = [{
193+
matcher = "";
194+
hooks = [{ type = "command"; command = "${config.home.homeDirectory}/.claude/hooks/peon-ping/peon.sh"; timeout = 10; async = true; }];
195+
}];
196+
Notification = [{
197+
matcher = "";
198+
hooks = [{ type = "command"; command = "${config.home.homeDirectory}/.claude/hooks/peon-ping/peon.sh"; timeout = 10; async = true; }];
199+
}];
200+
PermissionRequest = [{
201+
matcher = "";
202+
hooks = [{ type = "command"; command = "${config.home.homeDirectory}/.claude/hooks/peon-ping/peon.sh"; timeout = 10; async = true; }];
203+
}];
204+
PostToolUseFailure = [{
205+
matcher = "Bash";
206+
hooks = [{ type = "command"; command = "${config.home.homeDirectory}/.claude/hooks/peon-ping/peon.sh"; timeout = 10; async = true; }];
207+
}];
208+
PreCompact = [{
209+
matcher = "";
210+
hooks = [{ type = "command"; command = "${config.home.homeDirectory}/.claude/hooks/peon-ping/peon.sh"; timeout = 10; async = true; }];
211+
}];
212+
};
213+
}}'
214+
215+
settings_path="$HOME/.claude/settings.json"
216+
mkdir -p "$(dirname "$settings_path")"
217+
218+
${pkgs.python3}/bin/python3 - "$settings_path" "$hooks_json" <<'PY'
219+
import json
220+
import pathlib
221+
import sys
222+
223+
settings_path = pathlib.Path(sys.argv[1]).expanduser()
224+
hooks_path = pathlib.Path(sys.argv[2])
225+
226+
if settings_path.exists():
227+
settings = json.loads(settings_path.read_text())
228+
else:
229+
settings = {}
230+
231+
incoming = json.loads(hooks_path.read_text())["hooks"]
232+
hooks = settings.setdefault("hooks", {})
233+
234+
def command_contains(entry, needles):
235+
return any(any(needle in hook.get("command", "") for needle in needles) for hook in entry.get("hooks", []))
236+
237+
for event, event_hooks in incoming.items():
238+
existing = hooks.get(event, [])
239+
if event == "UserPromptSubmit":
240+
existing = [
241+
entry for entry in existing
242+
if not command_contains(entry, ("peon.sh", "hook-handle-use", "hook-handle-rename"))
243+
]
244+
elif event == "PostToolUseFailure":
245+
existing = [entry for entry in existing if not command_contains(entry, ("peon.sh", "notify.sh"))]
246+
else:
247+
existing = [entry for entry in existing if not command_contains(entry, ("peon.sh", "notify.sh"))]
248+
hooks[event] = existing + event_hooks
249+
250+
if event == "UserPromptSubmit" and "beforeSubmitPrompt" in hooks:
251+
hooks.pop("beforeSubmitPrompt", None)
252+
253+
settings["hooks"] = hooks
254+
settings_path.write_text(json.dumps(settings, indent=2) + "\n")
255+
PY
256+
'');
257+
144258
# Install sound packs from og-packs and/or custom sources
145259
home.file.".openpeon/packs" = lib.mkIf (cfg.installPacks != [ ]) (let
146260
# Separate string pack names (og-packs) from custom pack specs

tests/nix-home-manager.bats

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/usr/bin/env bats
2+
3+
setup() {
4+
REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
5+
MODULE="$REPO_ROOT/nix/hm-module.nix"
6+
}
7+
8+
@test "home-manager module exposes Claude Code integration option" {
9+
grep -q 'claudeCodeIntegration = mkOption' "$MODULE"
10+
}
11+
12+
@test "home-manager Claude Code integration installs hook files and settings merge" {
13+
grep -q '".claude/hooks/peon-ping/peon.sh".source' "$MODULE"
14+
grep -q '".claude/hooks/peon-ping/scripts/hook-handle-use.sh".source' "$MODULE"
15+
grep -q 'settings_path=\"\$HOME/.claude/settings.json\"' "$MODULE"
16+
grep -q 'hooks.pop("beforeSubmitPrompt", None)' "$MODULE"
17+
}

0 commit comments

Comments
 (0)