cd /news/developer-tools/sourcetree-ai-commit-message · home topics developer-tools article
[ARTICLE · art-56908] src=gist.github.com ↗ pub= topic=developer-tools verified=true sentiment=· neutral

SourceTree Ai Commit message

A developer created a SourceTree custom action that uses Anthropic's Claude CLI to automatically generate commit messages from staged diffs. The script enforces Conventional Commits format, truncates large diffs, and presents the message in a dialog for review before committing.

read4 min views1 publishedJul 12, 2026

| #!/bin/bash | | | # SourceTree Custom Action: generates a commit message with Claude from the | | | # staged files and commits directly. | | | # Usage from SourceTree: Parameter = $REPO | | | set -euo pipefail | | | # GUI apps on macOS (SourceTree included) start with a minimal PATH, so we | | | # need to manually add the paths where the claude binary might live. | | | export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$PATH" | | | REPO="${1:-}" | | | if [ -z "$REPO" ]; then | | | echo "Error: missing repository path (use $REPO as the Custom Action parameter)." | | | exit 1 | | | fi | | | cd "$REPO" | | | if git diff --cached --quiet; then | | | echo "No staged files. Run 'git add' on your changes before running this action." | | | exit 1 | | | fi | | | CACHE_DIR="$HOME/.cache/sourcetree-ai-commit" | | | mkdir -p "$CACHE_DIR" | | | CACHE_KEY=$(printf '%s' "$REPO" | tr '/' '_') | | | CACHE_FILE="$CACHE_DIR/${CACHE_KEY}.txt" | | | generate_message() { | | | if ! command -v claude >/dev/null 2>&1; then | | | echo "Error: 'claude' CLI not found in PATH." | | | exit 1 | | | fi | | | STAT=$(git diff --cached --stat) | | | RAW_DIFF=$(git diff --cached -- . \ | | | ':(exclude).lock' \ | | | ':(exclude).resolved' \ | | | ':(exclude).xcworkspace/**' \ | | | ':(exclude).pbxproj') | | | # Truncate with bash parameter expansion instead of "| head -c": with | | | # pipefail enabled, head -c closes the pipe as soon as it has enough | | | # bytes, git diff gets SIGPIPE and the whole pipeline "fails" (exit 141), | | | # killing the script with no output because of set -e. | | | DIFF="${RAW_DIFF:0:12000}" | | | PROMPT="You are a commit message generator. Analyze the staged diff and return ONLY the commit message, with no explanations and no surrounding quotes. | | | Rules: | | | - Conventional Commits format (feat, fix, refactor, chore, docs, test, style, perf) followed by ':'. | | | - English, imperative mood, subject line max 72 characters. | | | - If the change warrants it, add a short bulleted body after a blank line explaining 'why', not 'what'. | | | - If there are several unrelated changes, prioritize the most relevant one in the subject. | | | - Plain text only: no markdown whatsoever — no backticks, no bold/italic with * or _, no headers, no code fences. Write identifiers (class/function names, etc.) as plain words, not wrapped in special quoting. | | | File summary: | | | ${STAT} | | | Diff: | | | ${DIFF}" | | | MESSAGE=$(claude -p "$PROMPT" --output-format text 2>/dev/null | sed -e '/[1]$/d' -e '1{/^/d;}' -e '${/^$/d;}' -e 's/`//g') | | | if [ -z "$MESSAGE" ]; then | | | echo "Error: Claude did not return a message." | | | exit 1 | | | fi | | | } | | | if [ -f "$CACHE_FILE" ]; then | | | MESSAGE=$(<"$CACHE_FILE") | | | else | | | generate_message | | | fi | | | escape_for_applescript() { | | | local input="$1" | | | input="${input//\/\\}" | | | input="${input//"/\"}" | | | printf '%s' "$input" | | | } | | | while true; do | | | ESCAPED_MESSAGE=$(escape_for_applescript "$MESSAGE") | | | # AppleScript doesn't interpret "\n" inside a string: each line break must be | | | # concatenated explicitly as the ASCII 10 character between literals. | | | AS_DEFAULT_ANSWER=$(printf '%s' "$ESCAPED_MESSAGE" | awk 'BEGIN{ORS=""} {if(NR>1) printf " & (ASCII character 10) & "; printf ""%s"", $0}') | | | set +e | | | DIALOG_RESULT=$(osascript <<EOF | | | set msgText to $AS_DEFAULT_ANSWER | | | set dialogResult to display dialog "Review and edit the commit message generated by Claude:" default answer msgText with title "Confirm commit" buttons {"Cancel", "Regenerate", "Commit"} default button "Commit" cancel button "Cancel" with icon note | | | return (button returned of dialogResult) & linefeed & (text returned of dialogResult) | | | EOF | | | ) | | | DIALOG_EXIT=$? | | | set -e | | | if [ $DIALOG_EXIT -ne 0 ]; then | | | echo "Commit canceled." | | | exit 0 | | | fi | | | # First line is the button name, everything after the first newline is | | | # the (possibly multi-line) edited message text. | | | DIALOG_BUTTON="${DIALOG_RESULT%%$'\n'}" | | | DIALOG_TEXT="${DIALOG_RESULT#*$'\n'}" | | | if [ "$DIALOG_BUTTON" = "Regenerate" ]; then | | | generate_message | | | continue | | | fi | | | MESSAGE="$DIALOG_TEXT" | | | break | | | done | | | TMP_MSG_FILE=$(mktemp) | | | trap 'rm -f "$TMP_MSG_FILE"' EXIT | | | echo "$MESSAGE" > "$TMP_MSG_FILE" | | | git commit -F "$TMP_MSG_FILE" | | | echo "$MESSAGE" > "$CACHE_FILE" | | | echo "Commit created with message:" | | | echo "---" | | | echo "$MESSAGE" |


  1. [:space:] ↩︎

── more in #developer-tools 4 stories · sorted by recency
── more on @sourcetree 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/sourcetree-ai-commit…] indexed:0 read:4min 2026-07-12 ·