← Back to list

🪝 Git’s pre-commit & commit-msg Hooks

From Zero to Your First Hook — The Essential Starting Point

J3 in Jungletronics · 2025-11-10 22:00 · 1 claps · 8.1 min read
#git-hooks #git-automation #dev-workflow #pre-commit-hook #commit-msg
Open on Medium ↗

🪝 Git’s pre-commit & commit-msg Hooks

From Zero to Your First Hook — The Essential Starting Point

Git hooks are tiny scripts that run automatically before or after specific Git actions, like committing or pushing. They help you enforce standards, prevent mistakes, and automate checks — right at the source.

In this guide, we start from stone zero: setting up simple local hooks, understanding how they trigger, and taking your first steps toward smarter automation inside your repositories.

Let’s Get Started

Note: if you get stuck, please see my repo.

  1. Create a new blank project in VS Code (name it however you like).
mkdir hook_test
cd hook_test
code .
  1. Open the integrated terminal and initialize Git:
git init
  1. Open the newly created .git directory
code .git

— inside it, you’ll find a hooks folder:

Expand it and you’ll see several sample hook files ( * . sample ).

Expand it and you’ll see several sample hook files ( . sample ).*

Alternatively use:

vim .git/hooks/ 

# To quit type: :q
  1. Let’s make our first hook! Just rename pre-commit.sample to pre-commit — basically, drop the .sample extension.

Autorize ¹ this script to run:

sudo chmod a+x .git/hooks/pre-commit

Our bash script will check for whitespace issues — if it finds any, it’ll list the problem files and block the commit.

Let’s give it a try!

5.Create a file (main.rb) and intentionally add some trailing spaces and lines to generate the whitespace error.

cat << 'EOF' > main.rb
puts "Hi from main!"
# TODO: This is a placeholder for credentials functionalities.
EOF

Intentionally adding lines and trailing spaces to activate the pre-commit hook.

Intentionally adding lines and trailing spaces to activate the pre-commit hook.

Save it.

6.Then run the first commit:

git add main.rb
git status
git commit -m "first commit"

You should see an ERROR:

Nice! The pre-commit hook just ran and stopped your first commit — exactly what we wanted.

git status

And you will see:

No commits yet

Let’s keep going!

Go ahead and clean up those extra lines and spaces, then try adding and committing again. This time, it should go through smoothly!

Now it should work fine!

  1. Rename pre-commit back to pre-commit.sample to disable it — we’ll create our own from scratch.

Inside .git/hooks, create a new file named pre-commit and paste the following:

touch .git/hooks/pre-commit
sudo chmod a+x .git/hooks/pre-commit

Paste inside the newly created file :

.git/hooks/**pre-commit**

#!/bin/sh

echo "Hello hooks!"

# --- Check for TODOs in staged files ---
if git diff --cached --name-only | xargs grep -Hn 'TODO' 2>/dev/null; then
  echo "⚠️ Commit warning: TODO comments found in the staged changes."
  # no exit here - just a warning
fi

Re-run the command (or use the File Explorer interface instead):

sudo chmod a+x .git/hooks/pre-commit

On some machines, we must force access to others. Don’t ask me why!

On some machines, we must force access to others. Don’t ask me why!

  1. Add this line to your main.rb file so you can stage the changes again:
# Let’s test our custom hook!
# TODO: refactor this
  1. Run your commit — the message from your hook should appear.
git add main.rb 
git commit -m "Second commit"
Hello hooks!
main.rb:2:# TODO: This is a placeholder for credentials functionalities.
⚠️ Commit warning: TODO comments found in the staged changes.
[master 84bae5b] second commit
 1 file changed, 1 insertion(+)

:/ There’s a TODO list waiting — let’s tackle it!

  1. Let’s walk through a quick test — saving hardcoded credentials to the repo. Never, ever do this in production !— I’ll show you exactly why it’s risky.

Delete TODO note.

Add this hardcoded snippet to your main.rb:

# Never, ever do this in production!
aws_access_key_id = "AKIA1234567890ABCDE"
aws_secret_access_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"

Now, let’s write a hook to prevent this type of commit from being pushed to the repository.

Past it inside .github/hooks/pre-commit :

...

# --- Check for hardcoded secrets in staged content only ---
if git diff --cached | grep -I -n -i -E "(key|secret|token|password)\s*[:=]\s*['\"]?[A-Za-z0-9/+=._-]+"; then
  echo "❌ Commit aborted: Hardcoded secret detected!"
  exit 1
fi

exit 0

Let’s run the hook and watch it work:

git add main.rb
git commit -m "Committing secrets"
Hello hooks!
10:+aws_secret_access_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
❌ Commit aborted: Hardcoded secret detected!

Your commit will be blocked — exactly what we want! 😤

To make it work, let’s apply the same techniques from this post — but this time, using pure Ruby.

  1. Create a script that can encrypt and decrypt the credentials:
touch encrypt_secrets.rb

encrypt_secrets.rb


#!/usr/bin/env ruby
require "json"
require "lockbox"

# --- Configuration ---
KEY = "c0a4b3ff1e4e1a22b89b0b93783d4ab9ff9f47b739b2d9a9df70e33a0b7f3a3e" # replace with your own
FILE = "./config/secrets.enc"

lockbox = Lockbox.new(key: KEY)

def usage
  puts "Usage:"
  puts "  ruby encrypt_secrets.rb --encrypt   # Encrypt config/secrets.json -> secrets.enc"
  puts "  ruby encrypt_secrets.rb --decrypt   # Decrypt config/secrets.enc -> print values"
  exit
end

mode = ARGV[0]
usage unless ["--encrypt", "--decrypt"].include?(mode)

if mode == "--encrypt"
  unless File.exist?("./config/secrets.json")
    puts "❌ secrets.json not found. Create one first, e.g.:"
    puts '{ "api_key": "abc123", "password": "supersecret" }'
    exit
  end

  data = File.read("./config/secrets.json")
  encrypted = lockbox.encrypt(data)
  File.write(FILE, encrypted)
  puts "✅ Encrypted and saved to #{FILE}"

elsif mode == "--decrypt"
  unless File.exist?(FILE)
    puts "❌ #{FILE} not found."
    exit
  end

  decrypted = lockbox.decrypt(File.read(FILE))
  secrets = JSON.parse(decrypted)
  puts "🔑 Decrypted secrets:"
  secrets.each { |k, v| puts "  #{k}: #{v}" }
end

This script provides functionality similar to the Rails credentials editor command: EDITOR="code --wait" rails credentials:edit.

12.Add dependency Lib:

touch Gemfile

Gemfile

source 'https://rubygems.org'

gem 'lockbox'

Run:

bundle install
  1. Create your .gitignore:Paste in your terminal:
cat << 'EOF' > .gitignore
# macOS-specific
.DS_Store
# Common Linux / VS Code / editor files
*~
*.swp
*.swo
# VS Code and JetBrains project settings
.vscode/
.idea/

# Local scripts and key material - never commit sensitive files
encrypt_secrets.rb
lockbox_key.txt
lockbox_key_base64.txt
encrypted_lockbox_key.txt
encrypted_lockbox_key_base64.txt
decrypt_lockbox_key_script.rb
encrypt_lockbox_key_script.rb
lockbox_test.rb
lockbox_test_decrypted.txt

# Ignore bundler config.
/.bundle
/vendor/bundle/
/vendor/cache/
Gemfile.lock

# Lockbox and Rails secret management files
config/initializers/lockbox.rb
config/master_key.txt
config/secrets.json
EOF

Git will ignore encrypt_secrets.rb and not track or commit it — as long as it hasn’t already been added to the repo.

If the file was never committed, you’re good — Git will skip it.

If it was already committed, you need to remove it from tracking first:

git rm --cached encrypt_secrets.rb
  1. Delete this from:

main.rb

# Never, ever do this in production!
aws_access_key_id = "AKIA1234567890ABCDE"
aws_secret_access_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
  1. Transfer the credentials to config/secrets.json file
mkdir config
touch config/secrets.json

secrets.json

{
  "aws_access_key_id": "AKJJHGBDBasadaaSLS",
  "aws_secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" 
}
  1. run
ruby encrypt_secrets.rb --encrypt
✅ Encrypted and saved to ./config/secrets.enc

Generates an encrypted ./config/secrets.enc file in the repository root directory.

You can commit it 🙌:

  • The secrets.enc file is securely encrypted using a strong script that's not stored in the repository.
  • The decryption code is safely kept on the developer’s machine (and excluded via .gitignore).
  • This setup allows collaborators or CI/CD pipelines to decrypt it securely when needed.

Great — now let’s move on to handling pre-commit messages.

  1. Let’s keep going!

In this step, we’ll create a commit-msg hook to enforce consistent commit message prefixes.

touch .git/hooks/commit-msg

.git/hooks/**commit-msg**

#!/usr/bin/env bash
# Simple commit message check for beginners

msg_file="$1"
msg=$(cat "$msg_file")

# Check if message is empty
if [[ -z "$msg" ]]; then
  echo "❌ Commit message cannot be empty."
  exit 1
fi

# Check if message is too short
if [[ ${#msg} -lt 10 ]]; then
  echo "⚠️ Commit message too short. Please describe your change briefly."
  exit 1
fi

# Enforce starting with a type like fix:, feat:, etc.
if ! [[ "$msg" =~ ^(feat|fix|docs|chore|test|refactor|style|perf): ]]; then
  echo "❌ Commit message must start with a valid type (feat:, fix:, ci:, docs:, chore:, test:, etc.)"
  echo "Example: feat: add user login form"
  exit 1
fi

# Optional: encourage a simple format like "type: short description"
# if ! [[ "$msg" =~ ^[a-zA-Z]+: ]]; then
#   echo "💡 Tip: Start your message with a type, e.g.: fix:, feat:, docs:, chore:, test:"
#   echo "Example: feat: add user login form"
# fi

exit 0

Give permissions:

sudo chmod a+x .git/hooks/commit-msg
  1. Modify:

main.rb

Puts "Message should start with a tyoe: fix, feat, docs, chore, test "
  1. Run on Terminal:
git add main.rb
git status
git commit -m "third commit"

Error:

Hello hooks!
❌ Commit message must start with a valid type (feat:, fix:, ci:, docs:, chore:, test:, etc.)
Example: feat: add user login form
  1. Run again, prefix w/ type:
git commit -m "test: third commit"

Now it is OK:

Hello hooks!
[master 6f884dc] test: third commit
 1 file changed, 2 insertions(+), 1 deletion(-)

Now for the final touch!

20. Promoting Local to Global Scope

Everything we did was local — but what if we want to make it global?

Image Credits goes for: Complete guide to GitHooks — Creating your own pre-commit hooks by  GitGuardian

Image Credits goes for: Complete guide to GitHooks — Creating your own pre-commit hooks by GitGuardian

Here is a neat way to promote your local Git hooks to global scope.

🧩 Commands (setup phase)

mkdir ~/.git
mkdir ~/.git/hooks

Creates a .git/hooks directory inside your home folder, which will serve as your global hooks directory — reusable across all repos.

touch ~/.git/hooks/pre-commit

Creates an empty pre-commit file that will contain your global hook logic.

chmod u+x ~/.git/hooks/*

Makes all files in the ~/.git/hooks folder executable, so Git can actually run them as scripts.

⚙️ Global hook configuration

git config --global core.hookspath ~/.git/hooks

Tells Git to look for hooks in this global directory (~/.git/hooks) instead of using each repo’s .git/hooks.

💡 Global pre-commit logic

Inside ~/.git/hooks/pre-commit:

#!bin/sh

echo "Hello Global Hooks!"
if [ -f .git/hooks/pre-commit ]; then
  if ! .git/hooks/pre-commit "$@"; then
    echo 'Local pre-commit hook failed; Please fix it before continuing!'
    exit 1
  fi
fi

Explanation:

  • #!bin/sh → Should actually be #!/bin/sh (note the missing slash). It specifies the shell interpreter.

  • echo "Hello Global Hooks!" → Prints a message confirming the global hook is running.

  • The if block checks whether the local repository still has its own .git/hooks/pre-commit.

  • If that local hook exists, it runs it: .git/hooks/pre-commit "$@"

  • If the local hook fails (exit code ≠ 0), it shows a warning and aborts the commit.

  • Otherwise, the global hook completes successfully.

In short: You’ve set up a global pre-commit hook manager that runs on every repo, optionally chaining to each repo’s local hook if present.

Summary:

mkdir ~/.git
mkdir ~/.git/hooks
touch ~/.git/hooks/pre-commit
chmod u+x ~/.git/hooks/*

then:

git config --global core.hookspath ~/.git/hooks

Thank you for reading! I appreciate you taking the time to explore this guide — your support means a lot. Wishing you success with your project ahead!

Note ¹

To give execution permission (or any permission) to other users — not just yourself — you use the chmod command with the o flag (for “others”).

1️⃣ Allow “others” to execute (run) the file:

chmod o+x .git/hooks/pre-commit

This means:

  • User (you) — no change
  • Group members — no change
  • Other users — now can execute the script

2️⃣ To let “others” also read (see the file contents):

chmod o+r .git/hooks/pre-commit

3️⃣ Combine both (read + execute):

chmod o+rx .git/hooks/pre-commit

🧠 Full permission overview

| Symbol | Stands for   | Example          | Meaning                             |
| ------ | ------------ | ---------------- | ----------------------------------- |
| `u`    | user (owner) | `chmod u+x file` | add execute for you                 |
| `g`    | group        | `chmod g+x file` | add execute for your group          |
| `o`    | others       | `chmod o+x file` | add execute for everyone else       |
| `a`    | all          | `chmod a+x file` | add execute for user, group, others |

⚙️ Typical Git hook setup

For Git hooks, the usual permission is:

chmod a+x .git/hooks/pre-commit

That ensures any user (including Git when running hooks) can execute the script.


메타데이터
post_id
9d541bb6dffd
slug
gits-pre-commit-commit-msg-hooks-9d541bb6dffd
url
https://medium.com/jungletronics/gits-pre-commit-commit-msg-hooks-9d541bb6dffd
canonical_url
https://medium.com/jungletronics/gits-pre-commit-commit-msg-hooks-9d541bb6dffd
author_url
https://medium.com/@jaythree
status
ok
fetched_at
2026-07-13 06:23:13