Writing commit messages is a chore. Conventional Commits adds strict rules. You can automate both with a free AI server. This tutorial builds a git hook that writes and validates commit messages.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
MonkeyCode is an open-source project. It offers a free server and free model access. The free tier currently includes 10 million tokens. That makes it practical for everyday git automation.
What you'll build
A commit-msg hook that runs on every commit. It checks your message against the Conventional Commits format. If the message is missing or invalid, it calls the AI. The AI writes a new message. The hook validates the result before accepting it.
Why commit messages matter
Good commit messages help debugging. They power changelogs. They make code review faster. A consistent format is worth automating.
Prerequisites
You need four things. Bash, curl, jq, and git. Plus a MonkeyCode account. That's it.
Step 1: Get your server URL
Create a MonkeyCode account. Start the free server from the dashboard. Copy the server URL. You will need it in the next step.
Set it as an environment variable.
export MONKEYCODE_API_URL="https://your-server-url"
Also set the model name.
export MONKEYCODE_MODEL="default"
Step 2: Write the hook script
Create a file named commit-msg in your project's .git/hooks directory. Paste the script below.
#!/usr/bin/env bash
set -euo pipefail
MSG_FILE="$1"
MSG="$(cat "$MSG_FILE")"
# Already a valid Conventional Commit?
if echo "$MSG" | grep -qE '^(feat|fix|chore|docs|style|refactor|perf|test|build|ci|revert)(\(.+\))?: .+'; then
exit 0
fi
API_URL="${MONKEYCODE_API_URL:-http://localhost:8080/v1/chat/completions}"
MODEL="${MONKEYCODE_MODEL:-default}"
DIFF="$(git diff --cached || true)"
if [ -z "$DIFF" ]; then
echo "No staged changes. Aborting." >&2
exit 1
fi
PROMPT="You are a git expert. Write a Conventional Commit message for this diff. Use the format type(scope): subject. Keep subject under 50 characters. Do not add a body unless necessary. Diff:
$DIFF"
RESPONSE="$(curl -s "$API_URL" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg model "$MODEL" --arg prompt "$PROMPT" '{model:$model, messages:[{role:"user", content:$prompt}]}')" \
| jq -r '.choices[0].message.content')"
# Extract the first line that matches the Conventional Commits format
GENERATED="$(echo "$RESPONSE" | grep -E '^(feat|fix|chore|docs|style|refactor|perf|test|build|ci|revert)(\(.+\))?: .+' | head -n1 || true)"
if [ -n "$GENERATED" ]; then
echo "$GENERATED" > "$MSG_FILE"
echo "Generated commit message: $GENERATED" >&2
else
echo "AI did not produce a valid message. Edit manually." >&2
exit 1
fi
Make it executable.
chmod +x .git/hooks/commit-msg
Step 3: Understand the flow
The hook runs after you write a commit message. It reads the message file. It checks the format. If the format is wrong, it builds a prompt from the staged diff. It sends the prompt to the AI server. It extracts a valid Conventional Commit from the response. It writes that message back to the file.
The grep pattern enforces the format. It allows types like feat, fix, and docs. It also allows an optional scope in parentheses.
Step 4: Test it
Stage a change and commit with an invalid message.
echo "test" > test.txt
git add test.txt
git commit -m "test"
The hook should replace the message with a generated one. You will see output like this.
Generated commit message: test: add test.txt
Check the log.
git log --oneline -1
You should see the generated message.
Step 5: Test with a valid message
Now commit with a valid Conventional Commit.
git commit --allow-empty -m "fix: resolve null pointer"
The hook should pass it through unchanged.
Step 6: Handle edge cases
The script has two failure modes. First, if there are no staged changes, it aborts. Second, if the AI returns no valid line, it exits with an error. In that case, git opens your editor so you can fix the message manually.
You can adjust the prompt. For example, ask for a body when the diff is large. Or restrict types to your project's convention.
Debugging the hook
If the hook does not run, check the file permissions. ls -l .git/hooks/commit-msg should show -rwxr-xr-x. If it does not, run chmod +x.
If the AI call fails, test the API manually.
curl -s "$MONKEYCODE_API_URL" \
-H "Content-Type: application/json" \
-d '{"model":"default","messages":[{"role":"user","content":"Say hi"}]}'
If you get a response, the server is fine. If not, check your URL and model name.
If the generated message is empty, check the prompt. The model may not follow the format. Add an example to the prompt.
PROMPT="You are a git expert. Write a Conventional Commit message for this diff. Example: fix(parser): handle empty input. Diff:
$DIFF"
This often improves output.
Limitations
The free tier has limits. Ten million tokens is generous, but not unlimited. Large diffs may exceed the context window. The AI may produce a message that does not match your team's style. You should review generated messages before pushing.
The script sends your diff to a remote server. Do not use it with proprietary code. If your project has strict compliance rules, use a private paid option.
Who should not use this
Do not use this if you need human-written messages for legal reasons. Do not use it for secrets or sensitive code. Do not use it as a substitute for code review.
Final check
Install the hook in a small repo. Run a few commits. Adjust the prompt to your style. That's it. You now have a free AI commit message writer.
Try it on your next feature branch. See if the messages match your team's conventions.
Top comments (1)
Using
git diff --cachedas the prompt and accepting only the first line matching thefeat|fix|...regex creates a clean loop, but the hook validates less than the prompt promises: it never enforces the 50-character subject limit or team-specific scope rules. I'd put generation inprepare-commit-msgand keepcommit-msgas a pure validator, since silently replacing an intentional message can discard context the diff doesn't contain. Because the full staged diff goes to a remote server, secret scanning, size caps, andcurl --fail --max-timewould make the failure boundary much safer.