Skip to content

Instantly share code, notes, and snippets.

@AndrewAltimit
Last active July 3, 2025 12:33
Show Gist options
  • Save AndrewAltimit/b6af33b2abfc42dfa9dcb149d9ed4f2e to your computer and use it in GitHub Desktop.
Save AndrewAltimit/b6af33b2abfc42dfa9dcb149d9ed4f2e to your computer and use it in GitHub Desktop.
Example of Claude Code Git Integration

Claude Code Git Integration

WARNING: CLAUDE CODE RUNS UNRESTRICTED - USE AT YOUR OWN RISK!

It is recommended to run this in an isolated sandbox environment like a VM with a github personal access token scoped to the bare minimum.

Overview

A custom implementation for automating GitHub issue handling using Claude Code locally. This solution monitors GitHub issues for mentions of "claude" and automatically creates branches, implements solutions, and opens pull requests.

This is a gist implementation meant for adaptation. Feel free to fork and modify for your specific needs.

Key Differences from Official Integration

While Anthropic provides an official Claude Code GitHub Action, this implementation offers a different approach:

Official Claude Code GitHub Action:

  • Runs within GitHub Actions workflows on GitHub-hosted runners
  • Triggered by GitHub events (issues, PRs, comments)
  • Requires GitHub App installation and repository secrets
  • Supports multiple auth methods (Anthropic API, AWS Bedrock, Google Vertex AI)
  • Provides a polished, production-ready solution with progress tracking
  • Works seamlessly with @claude mentions in issues and PRs

This Gist Implementation:

  • More control over the execution environment
  • Generic approach that can be adapted for other Git platforms
  • Self-contained bash scripts with minimal dependencies
  • Ideal for private repositories or when you want full control

Components

1. .env - Configuration File

Contains your GitHub Personal Access Token for authentication. Required scopes:

  • repo - Full control of private repositories
  • workflow - Update GitHub Action workflows
  • read:org - Read org and team membership

2. monitor_issues_local.sh - Issue Monitor

  • Continuously monitors GitHub issues for "claude" mentions
  • Runs directly on your local machine
  • Handles authentication via GitHub CLI (gh)
  • Tracks processed issues to avoid duplicates
  • Launches the handler script when relevant issues are found

3. claude_handler_local.sh - Claude Code Handler

  • Creates feature branches from issues
  • Invokes Claude Code with issue context
  • Commits changes and pushes to GitHub
  • Creates pull requests automatically
  • Provides status updates via issue comments

Setup Instructions

  1. Install Prerequisites:

    # Install GitHub CLI
    brew install gh  # macOS
    # or
    sudo apt-get install gh  # Ubuntu/Debian
    
    # Install Claude Code CLI
    npm install -g @anthropic-ai/claude-code
    
    # Ensure Node.js 22.16.0+ is available (via nvm)
    nvm install 22.16.0
    nvm use 22.16.0
  2. Configure Authentication:

    # Create .env file in project root
    echo "GITHUB_TOKEN=your_github_personal_access_token" > .env
    
    # Authenticate GitHub CLI
    gh auth login
  3. Set Permissions:

    chmod +x scripts/github-issue-monitor/monitor_issues_local.sh
    chmod +x scripts/github-issue-monitor/claude_handler_local.sh
  4. Run the Monitor:

    ./scripts/github-issue-monitor/monitor_issues_local.sh

How It Works

  1. Issue Detection: The monitor script polls GitHub for open issues containing "claude"
  2. Branch Creation: Creates a branch named claude-bot/issue-{number}
  3. Claude Invocation: Runs Claude Code with the issue context as a prompt
  4. Implementation: Claude analyzes the request and implements changes
  5. PR Creation: Automatically commits changes and opens a pull request

Use Cases

  • Local Development: Run Claude Code on your own hardware with full control
  • Private Repositories: Keep sensitive code processing entirely local
  • Custom Workflows: Easily modify scripts for specific needs
  • Platform Agnostic: Adapt for GitLab, Bitbucket, or other Git platforms

Advantages

  • No Infrastructure Required: Runs on your local machine without cloud services
  • Full Control: Complete visibility into what Claude Code is doing
  • Customizable: Bash scripts are easy to modify for specific workflows
  • Cost Effective: No GitHub Actions minutes consumed
  • Privacy: Code never leaves your local environment

Limitations

  • Requires local machine to be running for monitoring
  • No built-in CI/CD integration
  • Manual setup compared to official integration
  • Single-threaded processing of issues

Customization Tips

  • Modify CLAUDE_MENTION in the Python monitor to use different trigger words
  • Adjust the prompt template in claude_handler_local.sh for specific coding standards
  • Add pre/post-processing steps for testing or linting
  • Integrate with local CI tools for validation before PR creation

Security Considerations

  • Store GitHub tokens securely in .env (never commit)
  • Use repository-specific tokens with minimal permissions
  • Review Claude's changes before merging PRs
  • Consider running in an isolated environment for untrusted repositories

Troubleshooting

  • Authentication Issues: Verify GitHub token permissions and gh CLI login
  • Claude Code Failures: Check logs in logs/claude-handler/
  • Node.js Issues: Ensure correct version via nvm use 22.16.0
  • Branch Conflicts: Script cleans up old branches automatically

Note: This is a community implementation and not officially supported by Anthropic. For production use cases requiring reliability and support, consider the official Claude Code GitHub Action.

# =============================================================================
# GitHub Authentication (Required for workflow monitoring)
# =============================================================================
# Create a Personal Access Token at: https://github.com/settings/tokens
# GITHUB_TOKEN - Your Personal Access Token
# Required scopes for private repositories:
# ✅ repo (Full control of private repositories)
# ✅ workflow (Update GitHub Action workflows)
# ✅ read:org (Read org and team membership)
GITHUB_TOKEN=your_github_personal_access_token_here
#!/bin/bash
#
# Local Claude Code Handler for GitHub Issues
# Processes GitHub issues by creating branches and calling Claude Code directly
#
set -e
# Get environment variables passed from the monitor
ISSUE_NUMBER="${ISSUE_NUMBER}"
ISSUE_TITLE="${ISSUE_TITLE}"
ISSUE_AUTHOR="${ISSUE_AUTHOR}"
ISSUE_COMMAND="${ISSUE_COMMAND}"
REPO_OWNER="${REPO_OWNER}"
REPO_NAME="${REPO_NAME}"
GITHUB_TOKEN="${GITHUB_TOKEN}"
# Configuration
REPO="${REPO_OWNER}/${REPO_NAME}"
BRANCH_NAME="claude-bot/issue-${ISSUE_NUMBER}"
# Get project root
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
PROJECT_ROOT="$( cd "$SCRIPT_DIR/../.." && pwd )"
# Log file location
LOG_DIR="$PROJECT_ROOT/logs/claude-handler"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/handler-local-$(date +%Y%m%d).log"
# Function to log with timestamp
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
# Function to add a comment to the issue
add_issue_comment() {
local comment="$1"
log "Adding comment to issue #$ISSUE_NUMBER"
gh issue comment "$ISSUE_NUMBER" \
--repo "$REPO" \
--body "$comment"
}
# Function to get the default branch of the repository
get_default_branch() {
gh repo view "$REPO" --json defaultBranchRef --jq '.defaultBranchRef.name'
}
# Set up Node.js environment for Claude Code
setup_node_env() {
log "Setting up Node.js environment..."
# Load NVM if available
if [ -s "$HOME/.nvm/nvm.sh" ]; then
source "$HOME/.nvm/nvm.sh"
nvm use 22.16.0 >/dev/null 2>&1 || {
log "Installing Node.js 22.16.0..."
nvm install 22.16.0
nvm use 22.16.0
}
elif [ -s "/usr/local/share/nvm/nvm.sh" ]; then
source "/usr/local/share/nvm/nvm.sh"
nvm use 22.16.0 >/dev/null 2>&1 || {
log "Installing Node.js 22.16.0..."
nvm install 22.16.0
nvm use 22.16.0
}
fi
NODE_VERSION=$(node --version)
log "Using Node.js: $NODE_VERSION"
}
log "=== Processing Issue #$ISSUE_NUMBER Locally ==="
log "Title: $ISSUE_TITLE"
log "Author: $ISSUE_AUTHOR"
log "Command: $ISSUE_COMMAND"
log "Branch will be: $BRANCH_NAME"
# Get the default branch
DEFAULT_BRANCH=$(get_default_branch)
log "Repository default branch: $DEFAULT_BRANCH"
# First, acknowledge the issue
ACKNOWLEDGMENT="👋 Hello @$ISSUE_AUTHOR! I'm Claude, and I'll help you with this issue.
I'm analyzing your request and will create a branch to work on it locally. Here's what I understand:
**Request**: $ISSUE_COMMAND
I'll start working on this and provide updates as I progress. Creating branch \`$BRANCH_NAME\` from \`$DEFAULT_BRANCH\`..."
add_issue_comment "$ACKNOWLEDGMENT"
# Change to project root
cd "$PROJECT_ROOT"
# Clean up any stale git state
log "Cleaning up git state"
git reset --hard HEAD 2>/dev/null || true
git clean -fd 2>/dev/null || true
git checkout "$DEFAULT_BRANCH" 2>/dev/null || true
# Configure Git for GitHub operations
log "Configuring Git authentication"
git config --global user.name "Claude Bot"
git config --global user.email "[email protected]"
# Clear any existing credential helpers
git config --global --unset-all credential.helper || true
# Set up token-based authentication
if [ -n "$GITHUB_TOKEN" ]; then
log "Using GITHUB_TOKEN for authentication"
# Create credential helper that returns the token
git config --global credential.helper "!f() {
if [ \"\$1\" = \"get\" ]; then
echo \"username=token\"
echo \"password=$GITHUB_TOKEN\"
fi
}; f"
# Also set the remote URL to use token authentication
CURRENT_REMOTE=$(git config --get remote.origin.url || echo "")
if [[ "$CURRENT_REMOTE" == *"github.com"* ]] && [[ "$CURRENT_REMOTE" != *"@"* ]]; then
# Convert https://github.com/... to https://token:[email protected]/...
NEW_REMOTE=$(echo "$CURRENT_REMOTE" | sed "s|https://github.com/|https://token:[email protected]/|")
git remote set-url origin "$NEW_REMOTE"
log "Updated remote URL for token auth"
fi
else
log "Using gh CLI for authentication"
git config --global credential.helper "!gh auth git-credential"
fi
# Create and switch to the branch
log "Creating branch: $BRANCH_NAME"
# Clean up ALL existing claude-bot branch references (not just current)
log "Cleaning up all claude-bot branch references"
git branch -D $(git branch | grep "claude-bot" | xargs) 2>/dev/null || true
git branch -d -r $(git branch -r | grep "origin/claude-bot" | xargs) 2>/dev/null || true
rm -rf ".git/refs/remotes/origin/claude-bot" 2>/dev/null || true
rm -f ".git/refs/remotes/origin/claude-bot"* 2>/dev/null || true
find .git -name "*.lock" -delete 2>/dev/null || true
# Ensure git directories have proper permissions
mkdir -p .git/logs/refs/heads .git/refs/remotes/origin
chmod -R 755 .git/logs .git/refs 2>/dev/null || true
chown -R $(whoami):$(whoami) .git/logs .git/refs 2>/dev/null || true
# First switch away from default branch to avoid fetch conflicts
git checkout HEAD~0 2>/dev/null || true
# Fetch updates from origin
log "Fetching updates from origin"
git fetch origin || {
log "Error: Could not fetch from origin. Check GitHub authentication."
add_issue_comment "❌ Error: Could not fetch from GitHub repository. Please check authentication setup."
exit 1
}
# Switch to default branch first, then create new branch
git checkout "$DEFAULT_BRANCH" || {
log "Error: Could not switch to default branch $DEFAULT_BRANCH"
add_issue_comment "❌ Error: Could not switch to default branch \`$DEFAULT_BRANCH\`."
exit 1
}
git checkout -b "$BRANCH_NAME" || {
log "Error: Could not create branch $BRANCH_NAME"
add_issue_comment "❌ Error: Could not create branch \`$BRANCH_NAME\`. Please check for permission issues."
exit 1
}
# Update issue with execution status
EXECUTION_COMMENT="🚀 **Starting work on branch \`$BRANCH_NAME\`**
I'm now working on your request locally with Claude Code. I'll implement the changes and run tests before creating a pull request.
**Current status**: Analyzing request and implementing changes..."
add_issue_comment "$EXECUTION_COMMENT"
# Set up Node.js environment
setup_node_env
# Create a direct command prompt for Claude Code
CLAUDE_PROMPT_FILE="/tmp/claude_issue_${ISSUE_NUMBER}_prompt.txt"
cat > "$CLAUDE_PROMPT_FILE" << EOF
You are working on GitHub issue #$ISSUE_NUMBER: "$ISSUE_TITLE" within this repository.
REQUEST: $ISSUE_COMMAND
I need you to implement this request now. Here's what you should do:
1. Read the project structure and understand the codebase
2. Implement the specific request above
3. Follow existing patterns and conventions
4. Create or modify files as needed
5. Do not create placeholder files - implement actual functionality
The current branch is: $BRANCH_NAME
Start by exploring the codebase and then implement the request completely.
EOF
log "Launching Claude Code with issue prompt..."
# Set up environment variables for Claude Code automation
export CLAUDE_PROMPT_FILE="$CLAUDE_PROMPT_FILE"
export CLAUDE_AUTOMODE="true"
export CI="true"
export ANTHROPIC_AUTO_APPROVE="true"
export CLAUDE_NON_INTERACTIVE="true"
# Run Claude Code with the prompt file
log "Executing Claude Code in automated mode..."
CLAUDE_EXIT_CODE=0
# Try the simplest approach first - just use --print for text generation
log "Using Claude Code --print for text generation..."
timeout 960 claude --print --dangerously-skip-permissions < "$CLAUDE_PROMPT_FILE" > "/tmp/claude_output_${ISSUE_NUMBER}.txt" 2>&1 || CLAUDE_EXIT_CODE=$?
# If that fails, try a simple interactive approach with timeout
if [ $CLAUDE_EXIT_CODE -ne 0 ]; then
log "Text generation failed, trying interactive approach with extended timeout..."
CLAUDE_EXIT_CODE=0
# Simple approach: send prompt and exit command with extended timeout
timeout 1920 bash -c "
{
cat '$CLAUDE_PROMPT_FILE'
echo ''
echo 'Please complete this task and implement the changes needed.'
echo ''
# Give extended time for processing then send exit
sleep 160
echo 'exit'
} | claude --dangerously-skip-permissions
" || CLAUDE_EXIT_CODE=$?
fi
# Check if we got any output from the --print method
if [ -s "/tmp/claude_output_${ISSUE_NUMBER}.txt" ]; then
log "Claude Code --print generated output, creating response file"
# Create a response file with the generated content
echo "# Claude Code Response for Issue #$ISSUE_NUMBER" > "CLAUDE_RESPONSE_${ISSUE_NUMBER}.md"
echo "" >> "CLAUDE_RESPONSE_${ISSUE_NUMBER}.md"
echo "**Original Request**: $ISSUE_COMMAND" >> "CLAUDE_RESPONSE_${ISSUE_NUMBER}.md"
echo "" >> "CLAUDE_RESPONSE_${ISSUE_NUMBER}.md"
echo "**Generated Response**:" >> "CLAUDE_RESPONSE_${ISSUE_NUMBER}.md"
echo "" >> "CLAUDE_RESPONSE_${ISSUE_NUMBER}.md"
cat "/tmp/claude_output_${ISSUE_NUMBER}.txt" >> "CLAUDE_RESPONSE_${ISSUE_NUMBER}.md"
git add "CLAUDE_RESPONSE_${ISSUE_NUMBER}.md"
# Clean up temp file
rm -f "/tmp/claude_output_${ISSUE_NUMBER}.txt"
# Reset exit code since we got output
CLAUDE_EXIT_CODE=0
elif [ $CLAUDE_EXIT_CODE -ne 0 ]; then
log "Claude Code execution failed with exit code: $CLAUDE_EXIT_CODE"
# Determine the type of failure
if [ $CLAUDE_EXIT_CODE -eq 124 ]; then
FAILURE_REASON="Claude Code execution timed out"
elif [ $CLAUDE_EXIT_CODE -eq 1 ]; then
FAILURE_REASON="Claude Code encountered an error during execution"
else
FAILURE_REASON="Claude Code failed with exit code $CLAUDE_EXIT_CODE"
fi
# Create a detailed fallback implementation file
echo "# Implementation Status for Issue #$ISSUE_NUMBER" > "IMPLEMENTATION_ISSUE_${ISSUE_NUMBER}.md"
echo "" >> "IMPLEMENTATION_ISSUE_${ISSUE_NUMBER}.md"
echo "**Original Request**: $ISSUE_COMMAND" >> "IMPLEMENTATION_ISSUE_${ISSUE_NUMBER}.md"
echo "" >> "IMPLEMENTATION_ISSUE_${ISSUE_NUMBER}.md"
echo "**Status**: $FAILURE_REASON" >> "IMPLEMENTATION_ISSUE_${ISSUE_NUMBER}.md"
echo "**Timestamp**: $(date)" >> "IMPLEMENTATION_ISSUE_${ISSUE_NUMBER}.md"
echo "**Branch**: \`$BRANCH_NAME\`" >> "IMPLEMENTATION_ISSUE_${ISSUE_NUMBER}.md"
echo "" >> "IMPLEMENTATION_ISSUE_${ISSUE_NUMBER}.md"
echo "**Next Steps**:" >> "IMPLEMENTATION_ISSUE_${ISSUE_NUMBER}.md"
echo "- Check the logs in \`$LOG_FILE\`" >> "IMPLEMENTATION_ISSUE_${ISSUE_NUMBER}.md"
echo "- Verify Claude Code is properly installed and configured" >> "IMPLEMENTATION_ISSUE_${ISSUE_NUMBER}.md"
echo "- Consider manual implementation or re-running with different parameters" >> "IMPLEMENTATION_ISSUE_${ISSUE_NUMBER}.md"
git add "IMPLEMENTATION_ISSUE_${ISSUE_NUMBER}.md"
# Also notify in the issue comment
add_issue_comment "⚠️ **Claude Code Execution Failed**
$FAILURE_REASON
I've created a status file on branch \`$BRANCH_NAME\` with details. Please check the logs or try manual implementation."
# Clean up temp files
rm -f "/tmp/claude_output_${ISSUE_NUMBER}.txt"
fi
# Clean up prompt file
rm -f "$CLAUDE_PROMPT_FILE"
# Check what changes were made
log "Checking for changes..."
if git diff --quiet && git diff --staged --quiet; then
log "No changes were made by Claude Code"
FINAL_COMMENT="⚠️ **Implementation Incomplete**
I created a branch and ran Claude Code, but no changes were generated. The request was:
> $ISSUE_COMMAND
This might mean:
- The request needs more specific details
- Claude Code encountered an issue during execution
- Manual implementation is required
Please check the branch \`$BRANCH_NAME\` and provide more details if needed, or implement the changes manually."
else
log "Changes detected, committing and creating PR..."
# Add all changes
git add -A
# Commit the changes
git commit -m "Implement #$ISSUE_NUMBER: $ISSUE_TITLE
$ISSUE_COMMAND
🤖 Implemented locally with Claude Code" || {
log "Error: Could not commit changes"
add_issue_comment "❌ Error: Could not commit changes to branch \`$BRANCH_NAME\`."
exit 1
}
# Push the branch
log "Pushing branch to origin"
git push origin "$BRANCH_NAME" || {
log "Error: Could not push branch $BRANCH_NAME"
add_issue_comment "❌ Error: Could not push branch \`$BRANCH_NAME\` to GitHub. Please check permissions."
exit 1
}
# Create pull request
log "Creating pull request"
PR_URL=$(gh pr create \
--repo "$REPO" \
--title "Implement #$ISSUE_NUMBER: $ISSUE_TITLE" \
--body "Fixes #$ISSUE_NUMBER
**Original Request**: $ISSUE_COMMAND
**Implementation**: This PR was automatically generated by Claude Code running locally in response to the GitHub issue.
🤖 Implemented locally with Claude Code" \
--base "$DEFAULT_BRANCH" \
--head "$BRANCH_NAME" 2>&1) || {
log "Error creating pull request: $PR_URL"
add_issue_comment "❌ Error: Could not create pull request. Branch \`$BRANCH_NAME\` has been pushed with changes."
exit 1
}
# Clean up Claude response file after successful PR creation
if [ -f "CLAUDE_RESPONSE_${ISSUE_NUMBER}.md" ]; then
log "Removing Claude response file after successful PR creation"
git rm "CLAUDE_RESPONSE_${ISSUE_NUMBER}.md"
git commit -m "Clean up Claude response file
🤖 Automated cleanup after PR creation" || {
log "Warning: Could not remove Claude response file"
}
# Push the cleanup commit
git push origin "$BRANCH_NAME" || {
log "Warning: Could not push cleanup commit"
}
fi
FINAL_COMMENT="✅ **Implementation Complete!**
I've successfully implemented the requested changes using Claude Code and created a pull request:
**Pull Request**: $PR_URL
**Branch**: \`$BRANCH_NAME\`
The implementation has been completed locally and is ready for review. Please check the PR for details on what was implemented."
fi
add_issue_comment "$FINAL_COMMENT"
log "Issue #$ISSUE_NUMBER processing completed successfully"
exit 0
#!/bin/bash
#
# Local GitHub Issue Monitor with Automated Claude Code
# Runs directly on the host system without containers
#
set -e
# Get the directory where this script is located
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
PROJECT_ROOT="$( cd "$SCRIPT_DIR/../.." && pwd )"
# Load environment variables
if [ -f "$PROJECT_ROOT/.env" ]; then
source "$PROJECT_ROOT/.env"
fi
# Check if GITHUB_TOKEN is set
if [ -z "$GITHUB_TOKEN" ]; then
echo "Error: GITHUB_TOKEN not set in .env file"
exit 1
fi
# Log file location
LOG_DIR="$PROJECT_ROOT/logs/github-monitor"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/monitor-local-$(date +%Y%m%d).log"
# Function to log with timestamp
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
log "=== Starting Local GitHub Issue Monitor ==="
log "Project root: $PROJECT_ROOT"
log "Log file: $LOG_FILE"
# Check if gh CLI is installed
if ! command -v gh >/dev/null 2>&1; then
log "Installing GitHub CLI..."
# Install gh CLI based on system
if command -v brew >/dev/null 2>&1; then
brew install gh
elif command -v apt-get >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get install -y gh
elif command -v yum >/dev/null 2>&1; then
sudo yum install -y gh
else
log "Error: Could not install gh CLI automatically. Please install manually."
exit 1
fi
fi
# Set GH_TOKEN environment variable for non-interactive authentication
export GH_TOKEN="$GITHUB_TOKEN"
# Check if authentication works
if gh auth status &>/dev/null; then
log "GitHub CLI authenticated successfully"
else
log "Attempting to authenticate GitHub CLI..."
# Try to login with token if GH_TOKEN doesn't work
if ! echo "$GITHUB_TOKEN" | gh auth login --with-token --hostname github.com 2>/dev/null; then
log "Error: GitHub authentication failed"
log "Please ensure GITHUB_TOKEN is valid and has required permissions"
exit 1
fi
fi
# Check if Node.js environment is set up for Claude Code
log "Setting up Node.js environment for Claude Code..."
# Load NVM if available
if [ -s "$HOME/.nvm/nvm.sh" ]; then
log "Loading NVM..."
source "$HOME/.nvm/nvm.sh"
nvm use 22.16.0 2>/dev/null || {
log "Node.js 22.16.0 not found, installing..."
nvm install 22.16.0
nvm use 22.16.0
}
elif [ -s "/usr/local/share/nvm/nvm.sh" ]; then
log "Loading NVM (system-wide)..."
source "/usr/local/share/nvm/nvm.sh"
nvm use 22.16.0 2>/dev/null || {
log "Node.js 22.16.0 not found, installing..."
nvm install 22.16.0
nvm use 22.16.0
}
else
log "Warning: NVM not found, using system Node.js"
fi
# Verify Claude Code is available
if ! command -v claude >/dev/null 2>&1; then
log "Error: Claude Code CLI not found. Please install Claude Code first."
exit 1
fi
NODE_VERSION=$(node --version)
log "Using Node.js: $NODE_VERSION"
# Set up Git credentials for local use
log "Configuring Git for GitHub operations..."
# Clear any existing credential helpers
git config --global --unset-all credential.helper 2>/dev/null || true
if [ -n "$GITHUB_TOKEN" ]; then
log "Using GITHUB_TOKEN for git authentication"
# Set up token-based credential helper
git config --global credential.helper "!f() {
if [ \"\$1\" = \"get\" ]; then
echo \"username=token\"
echo \"password=$GITHUB_TOKEN\"
fi
}; f"
else
log "Using gh CLI for git authentication"
git config --global credential.helper "!gh auth git-credential"
fi
# Run the Python monitor script locally (not in container)
log "Running issue monitor locally..."
cd "$PROJECT_ROOT"
# Create a local version of the Python script that calls our local handler
python3 - << 'EOF'
#!/usr/bin/env python3
import json
import os
import sys
import re
from datetime import datetime
from pathlib import Path
import subprocess
import logging
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger('github-issue-monitor-local')
# Configuration
REPO_OWNER = "AndrewAltimit"
REPO_NAME = "ExampleRepo"
PROJECT_ROOT = os.getcwd()
STATE_FILE = os.path.join(PROJECT_ROOT, "scripts", "github-issue-monitor", "processed_issues.json")
CLAUDE_MENTION = "claude"
class GitHubIssueMonitor:
def __init__(self):
self.state_file = Path(STATE_FILE)
self.processed_issues = self.load_state()
def load_state(self):
"""Load previously processed issues from state file."""
if self.state_file.exists():
try:
with open(self.state_file, 'r') as f:
data = json.load(f)
return data.get('issue_states', {})
except Exception as e:
logger.error(f"Error loading state file: {e}")
return {}
return {}
def save_state(self):
"""Save processed issues to state file."""
try:
self.state_file.parent.mkdir(parents=True, exist_ok=True)
with open(self.state_file, 'w') as f:
json.dump({
'issue_states': self.processed_issues,
'last_updated': datetime.now().isoformat()
}, f, indent=2)
except Exception as e:
logger.error(f"Error saving state file: {e}")
def get_github_issues(self):
"""Fetch open issues from GitHub using gh CLI."""
try:
cmd = [
"gh", "issue", "list",
"--repo", f"{REPO_OWNER}/{REPO_NAME}",
"--state", "open",
"--json", "number,title,body,author,createdAt,updatedAt,labels,url",
"--limit", "100"
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
logger.error(f"Error fetching issues: {result.stderr}")
return []
return json.loads(result.stdout)
except Exception as e:
logger.error(f"Exception fetching issues: {e}")
return []
def get_issue_comments(self, issue_number):
"""Fetch comments for a specific issue."""
try:
cmd = [
"gh", "issue", "view", str(issue_number),
"--repo", f"{REPO_OWNER}/{REPO_NAME}",
"--json", "comments"
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
logger.error(f"Error fetching comments for issue #{issue_number}: {result.stderr}")
return []
data = json.loads(result.stdout)
return data.get('comments', [])
except Exception as e:
logger.error(f"Exception fetching comments: {e}")
return []
def check_claude_mention(self, issue):
"""Check if issue body, title, or comments contain 'claude' as a whole word."""
text_to_check = issue.get('body', '') + ' ' + issue.get('title', '')
comments = self.get_issue_comments(issue['number'])
for comment in comments:
text_to_check += ' ' + comment.get('body', '')
pattern = re.compile(r'\bclaude\b', re.IGNORECASE)
return bool(pattern.search(text_to_check))
def extract_claude_command(self, issue):
"""Extract the command/request from the issue including comments."""
body = issue.get('body', '')
title = issue.get('title', '')
comments = self.get_issue_comments(issue['number'])
comments_text = ""
for comment in comments:
author = comment.get('author', {}).get('login', 'Unknown')
comment_body = comment.get('body', '')
comments_text += f"\n\n--- Comment by {author} ---\n{comment_body}"
full_text = f"Title: {title}\n\nOriginal Issue:\n{body}{comments_text}".strip()
return full_text if full_text else None
def trigger_claude_code_local(self, issue):
"""Trigger Claude Code locally with issue data."""
try:
issue_number = issue['number']
issue_title = issue['title']
command = self.extract_claude_command(issue)
logger.info(f"Processing issue #{issue_number} locally with Claude Code")
# Call our local handler script
handler_script = os.path.join(PROJECT_ROOT, "scripts", "github-issue-monitor", "claude_handler_local.sh")
# Create the handler if it doesn't exist
if not os.path.exists(handler_script):
logger.error(f"Local handler script not found: {handler_script}")
return False
# Prepare environment
env = os.environ.copy()
env['GITHUB_TOKEN'] = os.getenv('GITHUB_TOKEN', '')
env['ISSUE_NUMBER'] = str(issue_number)
env['ISSUE_TITLE'] = issue_title
env['ISSUE_AUTHOR'] = issue['author']['login']
env['ISSUE_COMMAND'] = command
env['REPO_OWNER'] = REPO_OWNER
env['REPO_NAME'] = REPO_NAME
# Run the local handler
result = subprocess.run([handler_script], env=env, capture_output=True, text=True)
if result.returncode != 0:
logger.error(f"Error running local handler: {result.stderr}")
return False
logger.info(f"Successfully processed issue #{issue_number} locally")
return True
except Exception as e:
logger.error(f"Exception processing issue locally: {e}")
return False
def has_claude_pr(self, issue_number):
"""Check if Claude has already created a PR for this issue."""
try:
# Check if there's a branch for this issue
cmd = [
"gh", "api", f"repos/{REPO_OWNER}/{REPO_NAME}/branches/claude-bot/issue-{issue_number}"
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
# Branch exists, check if there's a PR for it
pr_cmd = [
"gh", "pr", "list",
"--repo", f"{REPO_OWNER}/{REPO_NAME}",
"--head", f"claude-bot/issue-{issue_number}",
"--json", "number",
"--limit", "1"
]
pr_result = subprocess.run(pr_cmd, capture_output=True, text=True)
if pr_result.returncode == 0:
prs = json.loads(pr_result.stdout)
return len(prs) > 0
except Exception as e:
logger.debug(f"Error checking for existing PR: {e}")
return False
def process_issues(self):
"""Main processing loop for checking and handling issues."""
logger.info("Starting local GitHub issue monitoring...")
issues = self.get_github_issues()
if not issues:
logger.info("No issues found or error fetching issues")
return
new_mentions = 0
for issue in issues:
issue_key = str(issue['number'])
issue_updated = issue['updatedAt']
last_processed = self.processed_issues.get(issue_key, "1970-01-01T00:00:00Z")
# Skip if Claude has already created a PR for this issue
if self.has_claude_pr(issue['number']):
logger.info(f"Skipping issue #{issue['number']} - Claude PR already exists")
# Update the timestamp to prevent future checking
self.processed_issues[issue_key] = issue_updated
continue
if issue_updated <= last_processed:
continue
if self.check_claude_mention(issue):
logger.info(f"Found new 'claude' mention in issue #{issue['number']}: {issue['title']}")
success = self.trigger_claude_code_local(issue)
self.processed_issues[issue_key] = issue_updated
if success:
new_mentions += 1
else:
self.processed_issues[issue_key] = issue_updated
self.save_state()
logger.info(f"Local monitoring complete. Found {new_mentions} new 'claude' mentions")
print(json.dumps({
'timestamp': datetime.now().isoformat(),
'new_mentions': new_mentions,
'total_processed': len(self.processed_issues)
}))
def main():
monitor = GitHubIssueMonitor()
monitor.process_issues()
if __name__ == "__main__":
main()
EOF
MONITOR_EXIT_CODE=$?
if [ $MONITOR_EXIT_CODE -eq 0 ]; then
log "Local issue monitor completed successfully"
else
log "Local issue monitor failed with exit code: $MONITOR_EXIT_CODE"
fi
log "=== Local GitHub Issue Monitor Complete ==="
exit $MONITOR_EXIT_CODE
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment