From 72ffd90bf307de1de945d3e1fc3ff10c8e36f386 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Tue, 17 Feb 2026 10:27:59 +0100 Subject: [PATCH] Add Claude hooks integration for AnviLLM - Created install-claude-hooks.sh to merge hooks into existing settings.json - Updated mkfile to use merge script instead of overwriting settings.json - Made agent name configurable via CLAUDE_AGENT_NAME env var - Added Claude agent configuration and hook scripts - Removed unused NewClaudeWithContinue/NewClaudeWithResume functions --- agents/claude/anvillm-agent.md | 7 ++++ agents/{ => kiro-cli}/anvillm-agent.json | 0 cfg/backends/claude.yaml | 3 ++ claude/hooks/anvillm-state-idle.sh | 31 ++++++++++++++++ claude/hooks/anvillm-state-running.sh | 31 ++++++++++++++++ claude/settings.json | 24 +++++++++++++ internal/backend/tmux/tmux.go | 11 +++--- internal/backends/claude.go | 24 ++++--------- mkfile | 10 ++++-- scripts/install-claude-hooks.sh | 45 ++++++++++++++++++++++++ 10 files changed, 163 insertions(+), 23 deletions(-) create mode 100644 agents/claude/anvillm-agent.md rename agents/{ => kiro-cli}/anvillm-agent.json (100%) create mode 100644 claude/hooks/anvillm-state-idle.sh create mode 100644 claude/hooks/anvillm-state-running.sh create mode 100644 claude/settings.json create mode 100644 scripts/install-claude-hooks.sh diff --git a/agents/claude/anvillm-agent.md b/agents/claude/anvillm-agent.md new file mode 100644 index 0000000..401fbcf --- /dev/null +++ b/agents/claude/anvillm-agent.md @@ -0,0 +1,7 @@ +--- +name: anvillm-agent +description: Agent for AnviLLM multi-agent workflows with Claude Code backend +permissionMode: bypassPermissions +--- + +You are an AI assistant for AnviLLM multi-agent workflows. You have full access to all tools and operate without permission restrictions. diff --git a/agents/anvillm-agent.json b/agents/kiro-cli/anvillm-agent.json similarity index 100% rename from agents/anvillm-agent.json rename to agents/kiro-cli/anvillm-agent.json diff --git a/cfg/backends/claude.yaml b/cfg/backends/claude.yaml index ae7cb25..d66004e 100644 --- a/cfg/backends/claude.yaml +++ b/cfg/backends/claude.yaml @@ -4,7 +4,10 @@ filesystem: - "{HOME}/.claude" - "{HOME}/.claude.json" - "{HOME}/.config/anvillm" + rwx: + - "{HOME}/.claude/hooks" network: unrestricted: true env: - ANTHROPIC_API_KEY + - CLAUDE_AGENT_NAME=anvillm-agent diff --git a/claude/hooks/anvillm-state-idle.sh b/claude/hooks/anvillm-state-idle.sh new file mode 100644 index 0000000..85b6601 --- /dev/null +++ b/claude/hooks/anvillm-state-idle.sh @@ -0,0 +1,31 @@ +#!/bin/sh +echo "$(date '+%Y-%m-%d %H:%M:%S') - Hook fired: Stop (PPID=$PPID)" >> /tmp/claude-hooks.log +cat > /dev/null +pid=$PPID +while [ $pid -gt 1 ]; do + if ps -p $pid -o comm= | grep -q claude; then + break + fi + pid=$(ps -p $pid -o ppid= | tr -d ' ') +done +if [ $pid -le 1 ]; then + echo "$(date '+%Y-%m-%d %H:%M:%S') - ERROR: Claude PID not found" >> /tmp/claude-hooks.log + exit 0 +fi +echo "$(date '+%Y-%m-%d %H:%M:%S') - Found Claude PID: $pid" >> /tmp/claude-hooks.log +# Read session ID from filesystem directly (9p read doesn't work in sandbox) +ns_path="/tmp/ns.$USER.:0/anvillm-session-id-$pid" +if [ ! -f "$ns_path" ]; then + ns_path="/tmp/ns.$USER/anvillm-session-id-$pid" +fi +session=$(cat "$ns_path" 2>/dev/null) +if [ -z "$session" ]; then + echo "$(date '+%Y-%m-%d %H:%M:%S') - ERROR: Session ID not found for PID $pid" >> /tmp/claude-hooks.log + exit 0 +fi +echo "$(date '+%Y-%m-%d %H:%M:%S') - Found session ID: $session" >> /tmp/claude-hooks.log +if echo idle | 9p write agent/$session/state 2>>/tmp/claude-hooks.log; then + echo "$(date '+%Y-%m-%d %H:%M:%S') - SUCCESS: Wrote 'idle' to agent/$session/state" >> /tmp/claude-hooks.log +else + echo "$(date '+%Y-%m-%d %H:%M:%S') - ERROR: Failed to write to agent/$session/state" >> /tmp/claude-hooks.log +fi diff --git a/claude/hooks/anvillm-state-running.sh b/claude/hooks/anvillm-state-running.sh new file mode 100644 index 0000000..acfd8de --- /dev/null +++ b/claude/hooks/anvillm-state-running.sh @@ -0,0 +1,31 @@ +#!/bin/sh +echo "$(date '+%Y-%m-%d %H:%M:%S') - Hook fired: UserPromptSubmit (PPID=$PPID)" >> /tmp/claude-hooks.log +cat > /dev/null +pid=$PPID +while [ $pid -gt 1 ]; do + if ps -p $pid -o comm= | grep -q claude; then + break + fi + pid=$(ps -p $pid -o ppid= | tr -d ' ') +done +if [ $pid -le 1 ]; then + echo "$(date '+%Y-%m-%d %H:%M:%S') - ERROR: Claude PID not found" >> /tmp/claude-hooks.log + exit 0 +fi +echo "$(date '+%Y-%m-%d %H:%M:%S') - Found Claude PID: $pid" >> /tmp/claude-hooks.log +# Read session ID from filesystem directly (9p read doesn't work in sandbox) +ns_path="/tmp/ns.$USER.:0/anvillm-session-id-$pid" +if [ ! -f "$ns_path" ]; then + ns_path="/tmp/ns.$USER/anvillm-session-id-$pid" +fi +session=$(cat "$ns_path" 2>/dev/null) +if [ -z "$session" ]; then + echo "$(date '+%Y-%m-%d %H:%M:%S') - ERROR: Session ID not found for PID $pid" >> /tmp/claude-hooks.log + exit 0 +fi +echo "$(date '+%Y-%m-%d %H:%M:%S') - Found session ID: $session" >> /tmp/claude-hooks.log +if echo running | 9p write agent/$session/state 2>>/tmp/claude-hooks.log; then + echo "$(date '+%Y-%m-%d %H:%M:%S') - SUCCESS: Wrote 'running' to agent/$session/state" >> /tmp/claude-hooks.log +else + echo "$(date '+%Y-%m-%d %H:%M:%S') - ERROR: Failed to write to agent/$session/state" >> /tmp/claude-hooks.log +fi diff --git a/claude/settings.json b/claude/settings.json new file mode 100644 index 0000000..2bc1212 --- /dev/null +++ b/claude/settings.json @@ -0,0 +1,24 @@ +{ + "hooks": { + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "$HOME/.claude/hooks/anvillm-state-running.sh" + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "$HOME/.claude/hooks/anvillm-state-idle.sh" + } + ] + } + ] + } +} diff --git a/internal/backend/tmux/tmux.go b/internal/backend/tmux/tmux.go index a818fc2..230c894 100644 --- a/internal/backend/tmux/tmux.go +++ b/internal/backend/tmux/tmux.go @@ -349,12 +349,15 @@ func (b *Backend) CreateSession(ctx context.Context, opts backend.SessionOptions } sess.idleCond = sync.NewCond(&sess.mu) - // Write session ID to file for hook access (if kiro-cli backend) - if b.cfg.Name == "kiro-cli" && pid != 0 { + // Write session ID to file for hook access (if kiro-cli or claude backend) + if (b.cfg.Name == "kiro-cli" || b.cfg.Name == "claude") && pid != 0 { ns := client.Namespace() sessionIDFile := filepath.Join(ns, fmt.Sprintf("anvillm-session-id-%d", pid)) - os.WriteFile(sessionIDFile, []byte(id), 0644) - debug.Log("[session %s] wrote session ID file: %s", id, sessionIDFile) + if err := os.WriteFile(sessionIDFile, []byte(id), 0644); err != nil { + debug.Log("[session %s] failed to write session ID file: %v", id, err) + } else { + debug.Log("[session %s] wrote session ID file: %s", id, sessionIDFile) + } } // 10. Start reader goroutine diff --git a/internal/backends/claude.go b/internal/backends/claude.go index dcc32b0..0bed443 100644 --- a/internal/backends/claude.go +++ b/internal/backends/claude.go @@ -25,20 +25,15 @@ func NewClaude(nsSuffix string) backend.Backend { return newClaudeWithCommand([]string{"claude", "--dangerously-skip-permissions"}, nsSuffix) } -// NewClaudeWithContinue creates a backend that continues the most recent conversation -func NewClaudeWithContinue(nsSuffix string) backend.Backend { - return newClaudeWithCommand([]string{"claude", "--dangerously-skip-permissions", "--continue"}, nsSuffix) -} - -// NewClaudeWithResume creates a backend that resumes a specific session by ID -func NewClaudeWithResume(sessionID string, nsSuffix string) backend.Backend { - return newClaudeWithCommand([]string{"claude", "--dangerously-skip-permissions", "--resume", sessionID}, nsSuffix) -} - func newClaudeWithCommand(command []string, nsSuffix string) backend.Backend { + agentName := os.Getenv("CLAUDE_AGENT_NAME") + if agentName == "" { + agentName = "anvillm-agent" + } + return tmux.New(tmux.Config{ Name: "claude", - Command: command, + Command: append(command, "--agent", agentName), Environment: map[string]string{ "TERM": "xterm-256color", }, @@ -99,17 +94,12 @@ func (h *claudeCommands) Execute(ctx context.Context, command string) (string, e // // NOTE: Save/Load operations are NOT supported for Claude backend. // - Claude auto-saves all conversations to ~/.claude/projects//.jsonl +// - Sessions are automatically continued via --agent hook integration // - No practical way to "load" context from one session into another // - Context sharing would require either: // 1. Expensive message replay (burns tokens, slow) // 2. Lossy summarization (defeats purpose of context sharing) // 3. File manipulation (risky, undefined behavior) -// -// For session management: -// - Use NewClaudeWithResume(sessionID) to continue a specific session -// - Use NewClaudeWithContinue() to continue most recent session -// - Use ListSessions(cwd) to browse available sessions -// Claude automatically saves conversations to ~/.claude/projects//.jsonl // GetSessionDir returns the directory where Claude stores sessions for the given working directory func GetSessionDir(cwd string) string { diff --git a/mkfile b/mkfile index 0e0dcc2..b1ebe62 100644 --- a/mkfile +++ b/mkfile @@ -16,8 +16,14 @@ build:V: chmod 0755 $INSTALL_PATH/Workflows/* mkdir -p $HOME/.config/anvillm cp -rf cfg/* $HOME/.config/anvillm/ - mkdir -p $HOME/.kiro/agents - cp agents/* $HOME/.kiro/agents/ + mkdir -p $HOME/.kiro/agents/kiro-cli + mkdir -p $HOME/.claude/agents + mkdir -p $HOME/.claude/hooks + cp agents/kiro-cli/* $HOME/.kiro/agents/kiro-cli/ + cp agents/claude/* $HOME/.claude/agents/ + cp claude/hooks/* $HOME/.claude/hooks/ + chmod +x $HOME/.claude/hooks/*.sh + bash scripts/install-claude-hooks.sh install:V: build diff --git a/scripts/install-claude-hooks.sh b/scripts/install-claude-hooks.sh new file mode 100644 index 0000000..8c940bc --- /dev/null +++ b/scripts/install-claude-hooks.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +SETTINGS_FILE="$HOME/.claude/settings.json" +HOOKS_CONFIG='{ + "hooks": { + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "$HOME/.claude/hooks/anvillm-state-running.sh" + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "$HOME/.claude/hooks/anvillm-state-idle.sh" + } + ] + } + ] + } +}' + +if ! command -v jq &> /dev/null; then + echo "Error: jq is required but not installed. Please install jq first." >&2 + exit 1 +fi + +mkdir -p "$HOME/.claude" + +if [ -f "$SETTINGS_FILE" ]; then + # Merge with existing settings + jq -s '.[0] * .[1]' "$SETTINGS_FILE" <(echo "$HOOKS_CONFIG") > "$SETTINGS_FILE.tmp" + mv "$SETTINGS_FILE.tmp" "$SETTINGS_FILE" +else + # Create new settings file + echo "$HOOKS_CONFIG" | jq '.' > "$SETTINGS_FILE" +fi + +echo "Claude hooks installed to $SETTINGS_FILE"