Create a global git hook to auto-generate commit messages via llama-swap #99

Open
opened 2026-07-19 23:56:01 +00:00 by mcp-bot · 0 comments
Member

Goal

Create a prepare-commit-msg git hook that runs globally and calls the local llama-swap endpoint on energon.home.arpa to auto-generate conventional commit messages from the staged diff.

Background

Three approaches from the community:

  • Tom Dekan — shell function calling OpenRouter API with git diff HEAD piped in, using jq to build the JSON payload
  • Harper Reedprepare-commit-msg hook calling the llm CLI with a system prompt file, with spinner animation and error handling
  • AI Made Tools — Python script calling Ollama's /api/generate endpoint with Conventional Commits format

All three use the prepare-commit-msg hook, which fires after git creates the default commit message file but before the editor opens — perfect for pre-filling with an AI-generated message.

Approach

Architecture

git commit (no -m)
    │
    ▼
prepare-commit-msg hook
    │
    ├── git diff --cached  →  staged diff
    │
    ├── curl POST → http://energon.home.arpa:8080/api/v1/chat/completions
    │       model: <configured>
    │       prompt: system + diff
    │
    ▼
AI-generated commit message written to $1 (commit msg file)
    │
    ▼
editor opens with message pre-filled (user can edit or accept)

Implementation options

Option A: Shell script (simplest, no deps)

  • Pure bash, uses curl + jq
  • Pipes git diff --cached into a curl POST to llama-swap
  • Writes response to the commit message file

Option B: Python script (more robust)

  • Uses Python's subprocess + urllib (stdlib only)
  • Same flow as the AI Made Tools approach
  • Better JSON parsing and error handling

llama-swap endpoint

llama-swap exposes a chat completions API compatible with OpenAI's format:

POST http://energon.home.arpa:8080/api/v1/chat/completions
Content-Type: application/json

{
  "model": "<model-name>",
  "messages": [
    {"role": "system", "content": "<system prompt>"},
    {"role": "user", "content": "<diff + prompt>"}
  ]
}

Response:

{
  "choices": [
    {
      "message": {
        "content": "feat(k8s): add monitoring stack with Prometheus and Grafana"
      }
    }
  ]
}

System prompt (draft)

You are an expert programmer. Generate a concise git commit message in Conventional Commits format for the following staged changes.

Rules:
- First line: type(scope): description (max 72 chars)
- Types: feat, fix, docs, style, refactor, test, chore, build, ci, perf
- Scope is optional but preferred (use the most relevant directory/project)
- Add a blank line then a brief body if the change is non-trivial
- Do NOT wrap the message in a code block
- Output ONLY the commit message, nothing else
- Describe what changed and why, not just what files changed

Diff:

Hook setup (global)

# Create global hooks directory
mkdir -p ~/.git-hooks

# Create the hook
cat > ~/.git-hooks/prepare-commit-msg << 'HOOK'
#!/bin/sh

# Skip if SKIP_LLM_GITHOOK is set
[ -n "$SKIP_LLM_GITHOOK" ] && exit 0

# Skip merge/squash/amend commits
[ -n "$2" ] && exit 0

# Get the staged diff
diff=$(git diff --cached --diff-algorithm=minimal 2>/dev/null)
[ -z "$diff" ] && exit 0

# Truncate large diffs to avoid context limits
if [ ${#diff} -gt 8000 ]; then
  diff=$(echo "$diff" | head -c 8000)
  diff="$diff
... (diff truncated)"
fi

# Build the JSON payload
payload=$(jq -n \
  --arg model "${AI_COMMIT_MODEL:-qwen2.5-coder:7b}" \
  --arg system "$(cat ~/.config/prompts/commit-system-prompt.txt)" \
  --arg user "Diff:\n$diff" \
  '{
    model: $model,
    messages: [
      {role: "system", content: $system},
      {role: "user", content: $user}
    ]
  }')

# Call llama-swap
response=$(curl -s --max-time 30 \
  "http://energon.home.arpa:8080/api/v1/chat/completions" \
  -H "Content-Type: application/json" \
  -d "$payload" 2>&1)

# Extract the message
message=$(echo "$response" | jq -r '.choices[0].message.content' 2>/dev/null)

if [ -z "$message" ] || [ "$message" = "null" ]; then
  echo "AI commit msg generation failed, falling back to default" >&2
  exit 0
fi

# Prepend the generated message to the existing commit file
existing=$(cat "$1")
printf "%s\n%s\n" "$message" "$existing" > "$1"
HOOK

chmod +x ~/.git-hooks/prepare-commit-msg

# Tell git to use it
git config --global core.hooksPath ~/.git-hooks

Prompt file (~/.config/prompts/commit-system-prompt.txt)

Store the system prompt in a separate file so it can be iterated without touching the hook:

You are an expert programmer. Generate a concise git commit message in Conventional Commits format for the following staged changes.

Rules:
- First line: type(scope): description (max 72 chars)
- Types: feat, fix, docs, style, refactor, test, chore, build, ci, perf
- Scope is optional but preferred
- Add a blank line then a brief body if the change is non-trivial
- Do NOT wrap the message in a code block
- Output ONLY the commit message, nothing else
- Describe what changed and why

Diff:

Environment variables

Variable Default Description
SKIP_LLM_GITHOOK (unset) Set to skip the hook entirely
AI_COMMIT_MODEL qwen2.5-coder:7b Model name to use
AI_COMMIT_URL http://energon.home.arpa:8080 llama-swap base URL

Spinner (optional polish)

As Harper Reed's approach shows, a background spinner animation gives feedback while the model generates:

spin_animation() {
  spinner=("⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏")
  while true; do
    for i in "${spinner[@]}"; do
      printf "\r⏳ Generating commit message... %s" "$i"
      sleep 0.1
    done
  done
}

spin_animation &
spin_pid=$!

# ... make the API call ...

kill $spin_pid 2>/dev/null
wait $spin_pid 2>/dev/null
printf "\r✅ Done!                          \n"

Acceptance Criteria

  • Global prepare-commit-msg hook is installed at ~/.git-hooks/prepare-commit-msg
  • git config --global core.hooksPath points to the global hooks directory
  • Hook calls http://energon.home.arpa llama-swap endpoint with staged diff
  • Conventional Commits format is used for generated messages
  • Hook falls back gracefully if the LLM call fails (doesn't block commits)
  • SKIP_LLM_GITHOOK env var can disable the hook
  • Merge/squash/amend commits skip the hook
  • System prompt is stored externally for easy iteration
## Goal Create a `prepare-commit-msg` git hook that runs globally and calls the local llama-swap endpoint on `energon.home.arpa` to auto-generate conventional commit messages from the staged diff. ## Background Three approaches from the community: - [Tom Dekan](https://tomdekan.com/articles/ai-commit-messages) — shell function calling OpenRouter API with `git diff HEAD` piped in, using jq to build the JSON payload - [Harper Reed](https://harper.blog/2024/03/11/use-an-llm-to-automagically-generate-meaningful-git-commit-messages/) — `prepare-commit-msg` hook calling the `llm` CLI with a system prompt file, with spinner animation and error handling - [AI Made Tools](https://www.aimadetools.com/blog/build-ai-commit-message-generator/) — Python script calling Ollama's `/api/generate` endpoint with Conventional Commits format All three use the `prepare-commit-msg` hook, which fires after git creates the default commit message file but before the editor opens — perfect for pre-filling with an AI-generated message. ## Approach ### Architecture ``` git commit (no -m) │ ▼ prepare-commit-msg hook │ ├── git diff --cached → staged diff │ ├── curl POST → http://energon.home.arpa:8080/api/v1/chat/completions │ model: <configured> │ prompt: system + diff │ ▼ AI-generated commit message written to $1 (commit msg file) │ ▼ editor opens with message pre-filled (user can edit or accept) ``` ### Implementation options **Option A: Shell script (simplest, no deps)** - Pure bash, uses `curl` + `jq` - Pipes `git diff --cached` into a curl POST to llama-swap - Writes response to the commit message file **Option B: Python script (more robust)** - Uses Python's `subprocess` + `urllib` (stdlib only) - Same flow as the AI Made Tools approach - Better JSON parsing and error handling ### llama-swap endpoint llama-swap exposes a chat completions API compatible with OpenAI's format: ``` POST http://energon.home.arpa:8080/api/v1/chat/completions Content-Type: application/json { "model": "<model-name>", "messages": [ {"role": "system", "content": "<system prompt>"}, {"role": "user", "content": "<diff + prompt>"} ] } ``` Response: ```json { "choices": [ { "message": { "content": "feat(k8s): add monitoring stack with Prometheus and Grafana" } } ] } ``` ### System prompt (draft) ``` You are an expert programmer. Generate a concise git commit message in Conventional Commits format for the following staged changes. Rules: - First line: type(scope): description (max 72 chars) - Types: feat, fix, docs, style, refactor, test, chore, build, ci, perf - Scope is optional but preferred (use the most relevant directory/project) - Add a blank line then a brief body if the change is non-trivial - Do NOT wrap the message in a code block - Output ONLY the commit message, nothing else - Describe what changed and why, not just what files changed Diff: ``` ### Hook setup (global) ```bash # Create global hooks directory mkdir -p ~/.git-hooks # Create the hook cat > ~/.git-hooks/prepare-commit-msg << 'HOOK' #!/bin/sh # Skip if SKIP_LLM_GITHOOK is set [ -n "$SKIP_LLM_GITHOOK" ] && exit 0 # Skip merge/squash/amend commits [ -n "$2" ] && exit 0 # Get the staged diff diff=$(git diff --cached --diff-algorithm=minimal 2>/dev/null) [ -z "$diff" ] && exit 0 # Truncate large diffs to avoid context limits if [ ${#diff} -gt 8000 ]; then diff=$(echo "$diff" | head -c 8000) diff="$diff ... (diff truncated)" fi # Build the JSON payload payload=$(jq -n \ --arg model "${AI_COMMIT_MODEL:-qwen2.5-coder:7b}" \ --arg system "$(cat ~/.config/prompts/commit-system-prompt.txt)" \ --arg user "Diff:\n$diff" \ '{ model: $model, messages: [ {role: "system", content: $system}, {role: "user", content: $user} ] }') # Call llama-swap response=$(curl -s --max-time 30 \ "http://energon.home.arpa:8080/api/v1/chat/completions" \ -H "Content-Type: application/json" \ -d "$payload" 2>&1) # Extract the message message=$(echo "$response" | jq -r '.choices[0].message.content' 2>/dev/null) if [ -z "$message" ] || [ "$message" = "null" ]; then echo "AI commit msg generation failed, falling back to default" >&2 exit 0 fi # Prepend the generated message to the existing commit file existing=$(cat "$1") printf "%s\n%s\n" "$message" "$existing" > "$1" HOOK chmod +x ~/.git-hooks/prepare-commit-msg # Tell git to use it git config --global core.hooksPath ~/.git-hooks ``` ### Prompt file (`~/.config/prompts/commit-system-prompt.txt`) Store the system prompt in a separate file so it can be iterated without touching the hook: ``` You are an expert programmer. Generate a concise git commit message in Conventional Commits format for the following staged changes. Rules: - First line: type(scope): description (max 72 chars) - Types: feat, fix, docs, style, refactor, test, chore, build, ci, perf - Scope is optional but preferred - Add a blank line then a brief body if the change is non-trivial - Do NOT wrap the message in a code block - Output ONLY the commit message, nothing else - Describe what changed and why Diff: ``` ### Environment variables | Variable | Default | Description | |----------|---------|-------------| | `SKIP_LLM_GITHOOK` | (unset) | Set to skip the hook entirely | | `AI_COMMIT_MODEL` | `qwen2.5-coder:7b` | Model name to use | | `AI_COMMIT_URL` | `http://energon.home.arpa:8080` | llama-swap base URL | ### Spinner (optional polish) As Harper Reed's approach shows, a background spinner animation gives feedback while the model generates: ```bash spin_animation() { spinner=("⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏") while true; do for i in "${spinner[@]}"; do printf "\r⏳ Generating commit message... %s" "$i" sleep 0.1 done done } spin_animation & spin_pid=$! # ... make the API call ... kill $spin_pid 2>/dev/null wait $spin_pid 2>/dev/null printf "\r✅ Done! \n" ``` ## Acceptance Criteria - [ ] Global `prepare-commit-msg` hook is installed at `~/.git-hooks/prepare-commit-msg` - [ ] `git config --global core.hooksPath` points to the global hooks directory - [ ] Hook calls `http://energon.home.arpa` llama-swap endpoint with staged diff - [ ] Conventional Commits format is used for generated messages - [ ] Hook falls back gracefully if the LLM call fails (doesn't block commits) - [ ] `SKIP_LLM_GITHOOK` env var can disable the hook - [ ] Merge/squash/amend commits skip the hook - [ ] System prompt is stored externally for easy iteration
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
ops/homelab#99
No description provided.