Journey to Automating Markdown Frontmatter with Git Hooks
Beginning: The Problem Starts - Limitations of Pre-commit Hook
Managing frontmatter for each post manually while running a Markdown blog was tedious. Especially having to manually input metadata like commit messages, authors, and dates every single time.
Initially, I tried to solve this using a pre-commit hook.
# .husky/pre-commit (initial version)
# Update frontmatter for staged markdown files
Problems with Pre-commit Approach
But I soon discovered serious issues:
- Staging changes persist: After updating frontmatter, new changes always remained in Git staging
- Additional commit required: Another commit was needed to reflect the updated frontmatter
- No commit message access: Most critically, at the pre-commit stage, the commit message isn't finalized yet, so it couldn't be included in the frontmatter
// This is impossible in pre-commit
const commitMessage = "???"; // Doesn't exist yet
Development: Transition to Post-commit Hook
Realizing the limitations of pre-commit, I switched direction to a post-commit hook.
# .husky/post-commit
# Update frontmatter after commit + git commit --amend
Post-commit + Amend Approach
This approach worked as follows:
- User commits:
"β¨ Add new feature" - Post-commit hook executes
- Extract message from commit history and update frontmatter
- Include changes in existing commit with
git commit --amend --no-edit --no-verify
// scripts/update-frontmatter-postcommit.js
const changedFiles = execSync('git diff --name-only HEAD HEAD~1')
.trim().split('\n').filter(file => file.endsWith('.md'));
// ... update frontmatter ...
// Add changes to existing commit
execSync('git commit --amend --no-edit --no-verify');
Emergence of New Complexity
While the post-commit approach solved the commit message access problem, it introduced new issues:
- Infinite loop risk:
git commit --amendtriggers another post-commit hook - Complex prevention logic needed:
# Infinite loop prevention code if [ "$FRONTMATTER_UPDATE_RUNNING" = "1" ]; then exit 0 fi export FRONTMATTER_UPDATE_RUNNING=1 - Git history tampering: SHA changes could cause conflicts during collaboration
- --no-verify limitations: Discovered that post-commit hooks can't be skipped with
--no-verify
Turning Point: Searching for a Fundamental Solution
Looking at the complicated code, I thought there must be a better way. After deeper investigation into Git hooks, I discovered the perfect solution: prepare-commit-msg hook.
Discovering prepare-commit-msg Hook
Through web research, I confirmed the exact order of Git commit workflow:
- Stage files (
git add) - Execute pre-commit hook
- Execute prepare-commit-msg hook β This is the key!
- Execute commit-msg hook
- Create actual commit
- Execute post-commit hook
Perfect Timing of Prepare-commit-msg
I discovered the characteristics of this hook:
- β Commit message is already finalized
- β Before the commit is created
- β Can modify files and re-stage
- β Automatically included in the same commit
# prepare-commit-msg hook parameters
# $1 = commit message file path
# $2 = commit source (message, template, merge, etc.)
CURRENT_MSG=$(cat "$1") # Can read commit message!
Resolution: Implementing the Perfect Solution
Final Implementation
I completely reimplemented using the prepare-commit-msg approach:
# .husky/prepare-commit-msg
#!/bin/sh
COMMIT_MSG_FILE="$1"
COMMIT_SOURCE="$2"
# Only run for regular commits
if [ "$COMMIT_SOURCE" = "message" ] || [ -z "$COMMIT_SOURCE" ]; then
CURRENT_MSG=$(cat "$COMMIT_MSG_FILE")
# Find staged markdown files
STAGED_MD_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.md$')
if [ -n "$STAGED_MD_FILES" ]; then
export COMMIT_MESSAGE="$CURRENT_MSG"
if pnpm exec node scripts/update-frontmatter-prepare.js; then
echo "β
Frontmatter updated and re-staged successfully"
fi
fi
fi
// scripts/update-frontmatter-prepare.js
const commitMessage = process.env.COMMIT_MESSAGE;
const stagedFiles = execSync('git diff --cached --name-only --diff-filter=ACM')
.trim().split('\n').filter(file => file && file.endsWith('.md'));
// Update frontmatter
const updatedData = {
title: title,
description: description,
authors: uniqueAuthors,
dates: dates,
messages: [commitMessage, ...existingMessages], // Include current commit message!
created: createdDate,
modified: modifiedDate
};
// Update file and auto re-stage
writeFileSync(filePath, updatedContent);
execSync(`git add "${file}"`); // Automatically included in same commit
Revolutionary Results
Advantages of the final implementation:
- Perfect timing: After commit message finalization, before commit creation
- Automatic inclusion: Re-staged changes naturally included in the same commit
- Complexity removed: No need for infinite loop prevention logic
- History integrity: No SHA changes, no amend needed
- Collaboration safe: Leverages Git's legitimate workflow
Real-world Example
$ git commit -m "β¨ Add new feature"
π Running pre-commit checks...
β
All pre-commit checks passed!
π Running prepare-commit-msg hook...
π Found staged markdown files: new-feature.md
β
new-feature.md frontmatter updated
β
Updated frontmatter has been staged.
π Validating commit message...
β
Commit message validation passed
[main abc1234] β¨ Add new feature
2 files changed, 50 insertions(+)
Resulting markdown:
---
title: New Feature
description: The innovative feature we just added
authors:
- XIYO
messages:
- 'β¨ Add new feature' # Automatically included!
createdAt: '2025-07-20T09:40:48.024Z'
modifiedAt: '2025-07-20T09:40:48.024Z'
modifiedAt: 2025-07-27T21:08:36+09:00
createdAt: 2025-07-22T02:44:08+09:00
---
# New Feature
The innovative feature we just added...
Conclusion: The Journey of Finding the Right Tool
Lessons learned from this project:
- Understanding the root of the problem: Not just finding a working solution, but understanding why the problem occurs
- Deep exploration of tools: Understanding the various types of Git hooks and their characteristics
- Complexity is a signal: If code becomes complex, there's likely a better way
- Power of the legitimate approach: Utilizing workflows aligned with Git's design intent
This journey from Pre-commit β Post-commit β Prepare-commit-msg was more than just a technical solutionβit was a valuable experience that made me think about the essence of problem-solving.
The frontmatter of this post was also automatically generated by the prepare-commit-msg hook! π