feat: add vmux setup and vmux hook for Claude Code integration
- vmux setup: idempotently adds hooks to ~/.claude/settings.json - vmux hook: receives Claude Code hook events via env vars and forwards to daemon - Daemon handles "hook" action on Unix socket (no HTTP server needed) - Hooks provide instant state detection (permission/question/idle)
This commit is contained in:
@@ -452,6 +452,15 @@ func (d *Daemon) handleConnection(conn net.Conn) {
|
||||
d.focus.Set(time.Duration(args.Minutes) * time.Minute)
|
||||
writeResponse(conn, Response{OK: true, FocusRemaining: d.focus.Remaining().Minutes()})
|
||||
|
||||
case "hook":
|
||||
var event HookEvent
|
||||
if err := json.Unmarshal(req.Args, &event); err != nil {
|
||||
writeResponse(conn, Response{Error: "invalid hook args: " + err.Error()})
|
||||
return
|
||||
}
|
||||
d.processHookEvent(event)
|
||||
writeResponse(conn, Response{OK: true})
|
||||
|
||||
case "stop":
|
||||
writeResponse(conn, Response{OK: true})
|
||||
d.Stop()
|
||||
|
||||
14
main.go
14
main.go
@@ -148,6 +148,18 @@ func main() {
|
||||
}
|
||||
runI3Bar(sockPath)
|
||||
|
||||
case "hook":
|
||||
event := parseHookEnv()
|
||||
if event.HookEventName == "" {
|
||||
os.Exit(0)
|
||||
}
|
||||
client := NewClient(sockPath)
|
||||
hookArgs, _ := json.Marshal(event)
|
||||
client.Send(Request{Action: "hook", Args: hookArgs})
|
||||
|
||||
case "setup":
|
||||
runSetup()
|
||||
|
||||
case "daemon":
|
||||
runDaemon(sockPath)
|
||||
|
||||
@@ -208,8 +220,8 @@ Commands:
|
||||
label <id> <text> Assign a label to a session
|
||||
focus <minutes> Suppress notifications for N minutes
|
||||
i3bar Output i3bar JSON (use as status_command in i3 config)
|
||||
setup Add vmux hooks to Claude Code settings (idempotent)
|
||||
stop Stop the vmux daemon
|
||||
daemon Run the daemon in foreground (used internally)
|
||||
|
||||
Flags:
|
||||
--no-color Disable colored output
|
||||
|
||||
106
setup.go
Normal file
106
setup.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// parseHookEnv builds a HookEvent from Claude Code hook environment variables.
|
||||
func parseHookEnv() HookEvent {
|
||||
return HookEvent{
|
||||
SessionID: os.Getenv("SESSION_ID"),
|
||||
Cwd: os.Getenv("SESSION_CWD"),
|
||||
HookEventName: os.Getenv("HOOK_EVENT_NAME"),
|
||||
NotificationType: os.Getenv("NOTIFICATION_TYPE"),
|
||||
ToolName: os.Getenv("TOOL_NAME"),
|
||||
}
|
||||
}
|
||||
|
||||
// runSetup adds vmux hooks to ~/.claude/settings.json (idempotent).
|
||||
func runSetup() {
|
||||
home := os.Getenv("HOME")
|
||||
settingsPath := filepath.Join(home, ".claude", "settings.json")
|
||||
|
||||
vmuxBin, err := os.Executable()
|
||||
if err != nil {
|
||||
vmuxBin = filepath.Join(home, "Code", "vibe", "vmux", "vmux")
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(settingsPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Cannot read %s: %v\n", settingsPath, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var settings map[string]interface{}
|
||||
if err := json.Unmarshal(data, &settings); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Cannot parse %s: %v\n", settingsPath, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
hooks, _ := settings["hooks"].(map[string]interface{})
|
||||
if hooks == nil {
|
||||
hooks = make(map[string]interface{})
|
||||
settings["hooks"] = hooks
|
||||
}
|
||||
|
||||
hookCommand := vmuxBin + " hook"
|
||||
vmuxHook := map[string]interface{}{
|
||||
"type": "command",
|
||||
"command": hookCommand,
|
||||
"timeout": 5,
|
||||
}
|
||||
|
||||
events := []string{"Notification", "Stop", "PostToolUse", "PreToolUse"}
|
||||
changed := false
|
||||
|
||||
for _, event := range events {
|
||||
entries, _ := hooks[event].([]interface{})
|
||||
|
||||
if hasVmuxHook(entries, hookCommand) {
|
||||
continue
|
||||
}
|
||||
|
||||
entry := map[string]interface{}{
|
||||
"hooks": []interface{}{vmuxHook},
|
||||
}
|
||||
|
||||
hooks[event] = append(entries, entry)
|
||||
changed = true
|
||||
}
|
||||
|
||||
if !changed {
|
||||
fmt.Println("vmux hooks already configured.")
|
||||
return
|
||||
}
|
||||
|
||||
out, err := json.MarshalIndent(settings, "", " ")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Cannot marshal settings: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(settingsPath, out, 0o644); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Cannot write %s: %v\n", settingsPath, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("vmux hooks added to %s for events: %v\n", settingsPath, events)
|
||||
fmt.Println("Restart Claude Code sessions for hooks to take effect.")
|
||||
}
|
||||
|
||||
func hasVmuxHook(entries []interface{}, hookCommand string) bool {
|
||||
for _, entry := range entries {
|
||||
e, _ := entry.(map[string]interface{})
|
||||
hookList, _ := e["hooks"].([]interface{})
|
||||
for _, h := range hookList {
|
||||
hm, _ := h.(map[string]interface{})
|
||||
if cmd, _ := hm["command"].(string); cmd == hookCommand {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user