Overview
Git hooks are executable scripts that Git runs at specific points in the version control workflow. Git stores these scripts in the .git/hooks directory of every repository. When a Git event occurs—such as committing, pushing, or receiving code—Git checks for the corresponding hook script and executes it if present.
The hook mechanism operates on a pass-fail basis. Client-side hooks run on the developer's local machine and can prevent operations like commits or pushes by exiting with a non-zero status code. Server-side hooks execute on the remote repository and can reject incoming changes. This binary nature makes hooks effective for automated quality gates and policy enforcement.
Git provides hook templates in new repositories, stored with a .sample extension. Removing the extension activates the hook. Git ignores hook files without execute permissions, which provides a method to temporarily disable hooks without deletion.
#!/usr/bin/env ruby
# .git/hooks/pre-commit
# Check for Ruby syntax errors
ruby_files = `git diff --cached --name-only --diff-filter=ACM | grep '.rb$'`.split("\n")
ruby_files.each do |file|
syntax_check = `ruby -c #{file} 2>&1`
unless $?.success?
puts "Syntax error in #{file}:"
puts syntax_check
exit 1
end
end
Hooks execute in the repository's root directory, regardless of where the Git command runs. The working directory context and environment variables differ between hook types, affecting how scripts access repository data.
Key Principles
Git divides hooks into two categories based on execution location. Client-side hooks run on the developer's machine during local operations: committing, merging, checking out code. Server-side hooks execute on the remote repository during network operations: receiving pushes, updating references.
Each hook has a specific invocation point in Git's execution sequence. Pre-event hooks run before Git performs an action, allowing validation and rejection. Post-event hooks execute after Git completes an action, enabling notifications and integrations. The timing determines what data the hook receives and what effects it can produce.
Git passes information to hooks through three mechanisms: command-line arguments, standard input, and environment variables. The pre-commit hook receives no arguments. The commit-msg hook receives the path to the commit message file. The pre-receive hook reads ref update information from stdin. Understanding these interfaces is critical for hook implementation.
Hooks cannot access configuration from the repository's .git/config directly during some operations. Git sets specific environment variables that hooks can read. The GIT_DIR variable points to the repository's .git directory. The GIT_INDEX_FILE variable specifies the index file location during some operations.
Exit codes determine hook success or failure. Zero indicates success and allows Git to proceed. Any non-zero value signals failure and aborts the operation for pre-event hooks. Post-event hooks execute after Git completes the operation, so their exit codes affect logging but not the outcome.
#!/usr/bin/env ruby
# .git/hooks/prepare-commit-msg
commit_msg_file = ARGV[0]
commit_source = ARGV[1]
sha = ARGV[2]
# Add branch name to commit message if not from merge/squash
if commit_source != 'merge' && commit_source != 'squash'
branch_name = `git symbolic-ref --short HEAD`.strip
if branch_name =~ /^(feature|bugfix|hotfix)\/([A-Z]+-\d+)/
ticket = $2
message = File.read(commit_msg_file)
unless message.include?(ticket)
File.open(commit_msg_file, 'w') do |f|
f.write("[#{ticket}] #{message}")
end
end
end
end
Hooks execute with the permissions and environment of the user running Git commands. This affects file access, network connectivity, and available tools. A hook that works for one developer may fail for another due to environment differences.
Git does not track hooks in version control by default. The .git directory contents are local to each repository clone. Teams must distribute hooks through external mechanisms: documentation, setup scripts, or third-party tools. This separation prevents malicious hooks from spreading through cloned repositories but creates synchronization challenges.
Ruby Implementation
Ruby excels as a hook implementation language due to its script execution model, string processing capabilities, and file manipulation tools. Ruby scripts start with a shebang line specifying the interpreter. The #!/usr/bin/env ruby shebang uses the system's Ruby installation, providing flexibility across environments.
Ruby hooks access Git data through system commands via backticks, %x(), or Open3 module. Backticks capture command output as a string and set $? to the process exit status. This pattern appears frequently in hook scripts.
#!/usr/bin/env ruby
# .git/hooks/pre-push
require 'open3'
remote = ARGV[0]
url = ARGV[1]
# Read push information from stdin
# Format: <local ref> <local sha> <remote ref> <remote sha>
$stdin.each_line do |line|
local_ref, local_sha, remote_ref, remote_sha = line.split
# Run tests before pushing to main branch
if remote_ref == 'refs/heads/main'
puts "Running test suite before pushing to main..."
stdout, stderr, status = Open3.capture3('bundle exec rspec')
unless status.success?
puts "Tests failed. Push aborted."
puts stdout
puts stderr
exit 1
end
end
end
The Ruby standard library provides tools for common hook operations. FileUtils handles file operations. Pathname simplifies path manipulation. YAML and JSON parse configuration files. Net::HTTP sends webhook notifications.
#!/usr/bin/env ruby
# .git/hooks/post-commit
require 'json'
require 'net/http'
commit_sha = `git rev-parse HEAD`.strip
commit_msg = `git log -1 --pretty=%B`.strip
author = `git log -1 --pretty=%an`.strip
webhook_url = ENV['COMMIT_WEBHOOK_URL']
if webhook_url
uri = URI(webhook_url)
payload = {
event: 'commit',
sha: commit_sha,
message: commit_msg,
author: author,
timestamp: Time.now.iso8601
}
Net::HTTP.post(uri, payload.to_json, 'Content-Type' => 'application/json')
end
Ruby gems extend hook capabilities. The git gem provides object-oriented Git access. The rugged gem offers libgit2 bindings for direct repository manipulation. The overcommit gem manages hook installation and configuration.
#!/usr/bin/env ruby
# .git/hooks/commit-msg
require 'English'
commit_msg_file = ARGV[0]
message = File.read(commit_msg_file)
# Enforce conventional commit format
CONVENTIONAL_PATTERN = /^(feat|fix|docs|style|refactor|perf|test|chore)(\(.+\))?: .{10,}/
unless message.match(CONVENTIONAL_PATTERN)
puts "Error: Commit message must follow conventional commits format"
puts "Format: <type>(optional scope): <description>"
puts ""
puts "Types: feat, fix, docs, style, refactor, perf, test, chore"
puts "Example: feat(auth): add OAuth2 login support"
exit 1
end
# Check for issue reference
unless message.match(/\b[A-Z]+-\d+\b/) || message.include?('no-issue')
puts "Warning: Commit message should reference an issue (e.g., PROJ-123)"
puts "Or include 'no-issue' if not applicable"
end
Ruby hooks handle errors through exceptions and exit codes. The exit method terminates with a status code. Calling exit 0 indicates success. Calling exit 1 or any non-zero value signals failure. The abort method prints an error message and exits with status 1.
Environment-specific behavior requires conditional logic. Checking for CI environment variables prevents hooks from running in continuous integration. Testing hook file permissions determines execution context.
#!/usr/bin/env ruby
# .git/hooks/pre-commit
# Skip hook in CI environment
if ENV['CI'] || ENV['GITHUB_ACTIONS'] || ENV['JENKINS_HOME']
exit 0
end
# Check for debugging output
staged_files = `git diff --cached --name-only --diff-filter=ACM`.split("\n")
ruby_files = staged_files.select { |f| f.end_with?('.rb') }
debug_patterns = [
/binding\.pry/,
/byebug/,
/debugger/,
/puts ['"]DEBUG/,
/p ['"]DEBUG/
]
ruby_files.each do |file|
content = File.read(file)
debug_patterns.each do |pattern|
if content.match(pattern)
puts "Error: Debugging statement found in #{file}"
puts "Pattern: #{pattern.inspect}"
exit 1
end
end
end
Implementation Approaches
Hook implementation spans individual developer enforcement to team-wide policy systems. The approach determines distribution method, configuration management, and maintenance burden.
Direct hook files provide the simplest implementation. Developers write executable scripts in .git/hooks directory. This approach works for personal workflows and single-developer projects. Each developer maintains their hooks independently. The main limitation is manual distribution—new team members must copy hook files manually.
Template-based distribution stores hooks in a tracked directory outside .git, typically hooks/ or scripts/hooks/ in the repository root. A setup script copies files to .git/hooks and sets execute permissions. This approach version-controls hook content while respecting Git's security model.
#!/usr/bin/env ruby
# scripts/setup-hooks.rb
require 'fileutils'
hooks_source = File.expand_path('../hooks', __dir__)
hooks_target = File.expand_path('../.git/hooks', __dir__)
Dir.glob("#{hooks_source}/*").each do |source_file|
next if File.directory?(source_file)
filename = File.basename(source_file)
target_file = File.join(hooks_target, filename)
FileUtils.cp(source_file, target_file)
FileUtils.chmod(0755, target_file)
puts "Installed #{filename}"
end
Configuration-driven hooks separate logic from policy. A single hook file reads configuration from a tracked YAML or JSON file, then executes checks based on settings. This allows teams to modify behavior without changing hook code.
#!/usr/bin/env ruby
# .git/hooks/pre-commit
require 'yaml'
config_file = File.expand_path('../../.hook-config.yml', __dir__)
config = YAML.load_file(config_file) if File.exist?(config_file)
if config && config['pre_commit']
if config['pre_commit']['check_syntax']
ruby_files = `git diff --cached --name-only --diff-filter=ACM | grep '.rb$'`.split("\n")
ruby_files.each do |file|
system("ruby -c #{file}") || exit(1)
end
end
if config['pre_commit']['run_linter']
system("bundle exec rubocop") || exit(1)
end
end
Wrapper-based systems install a single hook that delegates to multiple executable scripts. The wrapper searches a directory for script files and executes them in sequence. Any script failure aborts the operation. This approach supports plugin-like hook additions without modifying the wrapper.
#!/usr/bin/env ruby
# .git/hooks/pre-commit (wrapper)
hooks_dir = File.expand_path('../../hooks/pre-commit.d', __dir__)
if Dir.exist?(hooks_dir)
Dir.glob("#{hooks_dir}/*").sort.each do |hook_script|
next unless File.executable?(hook_script)
puts "Running #{File.basename(hook_script)}..."
system(hook_script) || exit(1)
end
end
Framework-based management uses tools like Overcommit, Husky (Node.js), or Lefthook. These tools provide hook installation, configuration management, and execution frameworks. They handle edge cases like missing dependencies and provide consistent cross-platform behavior.
Server-side hook implementation requires different considerations. Server hooks cannot abort operations by default in some Git hosting platforms. Custom Git servers need hook deployment pipelines. Repository managers like GitLab and GitHub provide webhook alternatives that offer similar functionality with better scalability.
Shared hook repositories centralize hook code across multiple projects. Teams maintain hooks in a separate repository, then reference them via submodules or package managers. This approach ensures consistency but adds dependency management complexity.
Practical Examples
A Ruby linting enforcement hook prevents commits that violate style guidelines. The hook runs RuboCop on staged files and blocks commits if violations exist. Running only on staged files keeps execution fast.
#!/usr/bin/env ruby
# .git/hooks/pre-commit
require 'open3'
# Get staged Ruby files
staged_files = `git diff --cached --name-only --diff-filter=ACM`.split("\n")
ruby_files = staged_files.select { |f| f.end_with?('.rb') }
exit 0 if ruby_files.empty?
puts "Running RuboCop on #{ruby_files.length} files..."
# Run RuboCop only on staged files
cmd = "bundle exec rubocop #{ruby_files.join(' ')}"
stdout, stderr, status = Open3.capture3(cmd)
puts stdout
puts stderr unless stderr.empty?
unless status.success?
puts "\nCommit aborted due to RuboCop violations."
puts "Fix the issues or use --no-verify to skip this check."
exit 1
end
puts "RuboCop check passed!"
A test execution hook ensures that code passes tests before pushing to shared branches. The hook reads push information from stdin and runs the test suite when pushing to protected branches. This prevents breaking builds.
#!/usr/bin/env ruby
# .git/hooks/pre-push
require 'open3'
protected_branches = ['main', 'master', 'develop']
$stdin.each_line do |line|
local_ref, local_sha, remote_ref, remote_sha = line.split
branch_name = remote_ref.sub('refs/heads/', '')
next unless protected_branches.include?(branch_name)
puts "Pushing to #{branch_name}. Running tests..."
stdout, stderr, status = Open3.capture3('bundle exec rspec')
unless status.success?
puts "\nTests failed!"
puts stdout
puts stderr
puts "\nPush aborted. Fix failing tests before pushing to #{branch_name}."
exit 1
end
puts "All tests passed!"
end
A commit message validation hook enforces message format standards. The hook checks that messages include required elements like issue references and maintain minimum length requirements.
#!/usr/bin/env ruby
# .git/hooks/commit-msg
commit_msg_file = ARGV[0]
message = File.read(commit_msg_file)
# Remove comments and blank lines
lines = message.lines.reject { |line| line.strip.start_with?('#') || line.strip.empty? }
cleaned_message = lines.join
errors = []
# Check minimum length
if cleaned_message.length < 10
errors << "Commit message too short (minimum 10 characters)"
end
# Check for subject line
subject = lines.first&.strip
if subject.nil? || subject.empty?
errors << "Commit message must have a subject line"
elsif subject.length > 72
errors << "Subject line too long (maximum 72 characters, got #{subject.length})"
end
# Check for issue reference
unless cleaned_message.match?(/\b[A-Z]+-\d+\b/) || cleaned_message.include?('[no-issue]')
errors << "Commit message must reference an issue (e.g., PROJ-123) or include [no-issue]"
end
# Check for imperative mood
if subject && subject.match?(/^(added|fixed|updated|changed|removed)/i)
errors << "Use imperative mood in subject line (e.g., 'Add feature' not 'Added feature')"
end
unless errors.empty?
puts "Commit message validation failed:"
errors.each { |error| puts " - #{error}" }
puts "\nCurrent message:"
puts cleaned_message
exit 1
end
A branch name validation hook prevents creation of branches that don't match naming conventions. The hook runs during checkout when creating new branches.
#!/usr/bin/env ruby
# .git/hooks/post-checkout
prev_head = ARGV[0]
new_head = ARGV[1]
branch_flag = ARGV[2]
# Only check on branch checkout
exit 0 unless branch_flag == '1'
branch_name = `git symbolic-ref --short HEAD 2>/dev/null`.strip
# Allow existing branches
exit 0 if branch_name.empty?
# Define valid branch patterns
valid_patterns = [
/^feature\/[A-Z]+-\d+-.+$/,
/^bugfix\/[A-Z]+-\d+-.+$/,
/^hotfix\/[A-Z]+-\d+-.+$/,
/^(main|master|develop)$/
]
valid = valid_patterns.any? { |pattern| branch_name.match?(pattern) }
unless valid
puts "\nInvalid branch name: #{branch_name}"
puts "\nBranch names must match one of these patterns:"
puts " - feature/TICKET-123-description"
puts " - bugfix/TICKET-123-description"
puts " - hotfix/TICKET-123-description"
puts " - main, master, or develop"
puts "\nRename your branch with:"
puts " git branch -m #{branch_name} <valid-name>"
# Don't abort on post-checkout, just warn
# exit 1
end
A dependency check hook validates that the working directory matches committed dependency files. This prevents commits that modify code without updating corresponding dependency manifests.
#!/usr/bin/env ruby
# .git/hooks/pre-commit
# Check if Gemfile changed but not Gemfile.lock
staged_files = `git diff --cached --name-only`.split("\n")
if staged_files.include?('Gemfile') && !staged_files.include?('Gemfile.lock')
puts "Error: Gemfile modified but Gemfile.lock not staged"
puts "Run 'bundle install' and stage Gemfile.lock"
exit 1
end
# Check if Gemfile.lock changed but not Gemfile
if staged_files.include?('Gemfile.lock') && !staged_files.include?('Gemfile')
puts "Error: Gemfile.lock modified but Gemfile not staged"
puts "This usually indicates a merge conflict or manual edit"
exit 1
end
# Verify Gemfile.lock is in sync
if staged_files.include?('Gemfile') || staged_files.include?('Gemfile.lock')
puts "Verifying bundle consistency..."
unless system('bundle check > /dev/null 2>&1')
puts "Error: Bundle is not consistent"
puts "Run 'bundle install' to update dependencies"
exit 1
end
end
Common Patterns
The guard clause pattern exits early when hook conditions don't apply. This keeps hook execution fast and reduces complexity. Guards check for required files, branch names, or environment variables before running expensive operations.
#!/usr/bin/env ruby
# .git/hooks/pre-commit
# Skip in CI
exit 0 if ENV['CI']
# Skip if no Ruby files changed
ruby_files = `git diff --cached --name-only | grep '.rb$'`.split("\n")
exit 0 if ruby_files.empty?
# Run checks only if guards passed
ruby_files.each do |file|
system("ruby -c #{file}") || exit(1)
end
The progressive validation pattern runs fast checks before slow checks. Syntax validation runs before linting. Linting runs before tests. This provides quick feedback and avoids unnecessary computation.
#!/usr/bin/env ruby
# .git/hooks/pre-commit
ruby_files = `git diff --cached --name-only | grep '.rb$'`.split("\n")
exit 0 if ruby_files.empty?
# Fast: Syntax check
puts "Checking syntax..."
ruby_files.each do |file|
system("ruby -c #{file} > /dev/null 2>&1") || abort("Syntax error in #{file}")
end
# Medium: Linting
puts "Running linter..."
system("bundle exec rubocop #{ruby_files.join(' ')}") || exit(1)
# Slow: Tests
puts "Running tests..."
system("bundle exec rspec") || exit(1)
The staged file pattern operates only on files in the staging area, not the working directory. This ensures hooks validate what Git will commit, not unsaved changes. The pattern uses git diff --cached to list staged files.
#!/usr/bin/env ruby
# .git/hooks/pre-commit
# Get staged content, not working directory content
staged_content = `git diff --cached`
# Or get staged file list
staged_files = `git diff --cached --name-only --diff-filter=ACM`.split("\n")
# For file-specific checks, use git show to get staged version
staged_files.each do |file|
staged_version = `git show :#{file}`
# Validate staged_version, not File.read(file)
end
The error accumulation pattern collects all validation errors before failing. This provides complete feedback in a single run instead of requiring multiple fix-and-retry cycles.
#!/usr/bin/env ruby
# .git/hooks/pre-commit
errors = []
# Check 1
ruby_files = `git diff --cached --name-only | grep '.rb$'`.split("\n")
ruby_files.each do |file|
unless system("ruby -c #{file} > /dev/null 2>&1")
errors << "Syntax error in #{file}"
end
end
# Check 2
unless system("bundle exec rubocop --format quiet")
errors << "RuboCop violations found"
end
# Check 3
unless system("bundle exec rspec --format progress")
errors << "Test failures found"
end
unless errors.empty?
puts "Commit blocked by the following issues:"
errors.each { |e| puts " - #{e}" }
exit 1
end
The configuration file pattern externalizes hook behavior to a tracked configuration file. This allows behavior modification without editing hook code.
#!/usr/bin/env ruby
# .git/hooks/pre-commit
require 'yaml'
config = YAML.load_file('.git-hooks.yml') rescue {}
if config.dig('pre_commit', 'enabled') == false
exit 0
end
checks = config.dig('pre_commit', 'checks') || []
checks.each do |check|
case check
when 'syntax'
# Run syntax check
when 'lint'
# Run linter
when 'test'
# Run tests
end
end
The bypass mechanism pattern allows developers to skip hooks when necessary while maintaining audit trails. The --no-verify flag bypasses hooks, but some teams log bypass usage.
#!/usr/bin/env ruby
# .git/hooks/pre-commit
# Check for bypass file
bypass_file = '.git/hooks/bypass-commit'
if File.exist?(bypass_file)
puts "Bypassing pre-commit hook (bypass file present)"
File.delete(bypass_file)
exit 0
end
# Normal validation...
The notification pattern sends alerts about repository events without blocking operations. Post-event hooks send webhook notifications, update issue trackers, or trigger CI pipelines.
#!/usr/bin/env ruby
# .git/hooks/post-commit
require 'net/http'
require 'json'
webhook_url = ENV['SLACK_WEBHOOK_URL']
exit 0 unless webhook_url
commit_msg = `git log -1 --pretty=%B`.strip
author = `git log -1 --pretty=%an`.strip
payload = {
text: "New commit by #{author}: #{commit_msg}"
}
uri = URI(webhook_url)
Net::HTTP.post(uri, payload.to_json, 'Content-Type' => 'application/json')
Common Pitfalls
Hooks fail silently when missing execute permissions. Git ignores hook files without executable bits set. Developers add hooks but forget chmod +x .git/hooks/pre-commit. The hook never runs, creating a false sense of security.
# Wrong: Hook won't execute
cp scripts/pre-commit .git/hooks/pre-commit
# Correct: Set execute permission
cp scripts/pre-commit .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
Working directory assumptions cause failures. Hooks execute in the repository root, but developers might invoke Git from subdirectories. Relative paths break when the working directory differs from expectations.
# Wrong: Assumes execution from repository root
config = YAML.load_file('.rubocop.yml')
# Correct: Use absolute paths
config_path = File.expand_path('../../.rubocop.yml', __dir__)
config = YAML.load_file(config_path)
Staged versus working directory confusion creates validation gaps. Hooks that check File.read(file) validate the working directory version, not the staged version Git will commit. Developers commit invalid code that passes their working directory but contains unstaged fixes.
# Wrong: Checks working directory
File.read('app/models/user.rb')
# Correct: Check staged version
`git show :app/models/user.rb`
Hook scripts without proper shebangs fail on some systems. Scripts need #!/usr/bin/env ruby at the beginning. Without a shebang, Git tries to execute the script as a shell script, causing errors.
Missing dependency checks cause runtime failures. Hooks that use RuboCop or RSpec fail when those gems are missing. The hook should verify dependencies exist before attempting execution.
#!/usr/bin/env ruby
# Check for required gems
begin
require 'rubocop'
rescue LoadError
puts "RuboCop not installed. Install with: gem install rubocop"
exit 0 # Exit success to allow commit
end
Environment differences between developers break hooks. A hook that works on one machine fails on another due to different Ruby versions, missing tools, or path configurations. Hooks must handle environment variability.
# Check if command exists before using it
def command_exists?(command)
system("which #{command} > /dev/null 2>&1")
end
unless command_exists?('rubocop')
puts "Warning: rubocop not found in PATH"
exit 0
end
Infinite loop scenarios occur when hooks trigger Git commands that invoke the same hook. A pre-commit hook that creates commits triggers itself recursively. Hooks must avoid Git operations that trigger themselves.
Long-running hooks frustrate developers. A hook that runs the entire test suite on every commit creates unbearable delays. Fast feedback requires selecting appropriate checks for each hook type. Run quick syntax checks in pre-commit, extensive tests in pre-push.
The --no-verify flag bypasses all client-side hooks. Teams cannot enforce policies that determined developers can circumvent. Server-side hooks or CI checks provide enforceable validation.
CI environment conflicts arise when hooks run in continuous integration. CI systems often need different behavior than local development. Hooks should detect CI environments and skip or modify checks accordingly.
# Skip expensive checks in CI
if ENV['CI'] || ENV['GITHUB_ACTIONS']
puts "Running in CI environment, skipping local-only checks"
exit 0
end
Merge commits create special cases. Some hooks receive different arguments during merge operations. The prepare-commit-msg hook receives merge as the commit source. Hooks must handle these variations.
commit_source = ARGV[1]
if commit_source == 'merge'
# Skip custom formatting for merge commits
exit 0
end
Binary file handling causes errors. Hooks that process file content fail on binary files. Pattern matching, syntax checking, and content validation need binary file detection.
def binary_file?(filepath)
content = File.read(filepath, 512)
content.encoding == Encoding::ASCII_8BIT && content.include?("\x00")
end
Subprocess failure handling requires careful status checking. System commands that fail might not abort the hook. Always check exit status when validation depends on external commands.
# Wrong: Ignores command failure
`rubocop app/models/user.rb`
# Correct: Check status
system("rubocop app/models/user.rb") || exit(1)
# Or use $? for more control
`rubocop app/models/user.rb`
exit(1) unless $?.success?
Reference
Hook Types and Execution Points
| Hook | When Executed | Arguments | Input |
|---|---|---|---|
| pre-commit | Before commit message editor | None | None |
| prepare-commit-msg | After default message created | msg-file, source, sha | None |
| commit-msg | After commit message entered | msg-file | None |
| post-commit | After commit completes | None | None |
| pre-rebase | Before rebase starts | upstream, branch | None |
| post-checkout | After checkout completes | prev-head, new-head, flag | None |
| post-merge | After merge completes | squash-flag | None |
| pre-push | Before push starts | remote-name, remote-url | refs on stdin |
| pre-receive | Before server accepts push | None | refs on stdin |
| update | Before updating each ref | ref-name, old-sha, new-sha | None |
| post-receive | After server accepts push | None | refs on stdin |
| post-update | After all refs updated | ref-names | None |
Common Exit Codes
| Code | Meaning | Effect |
|---|---|---|
| 0 | Success | Operation continues |
| 1 | General failure | Operation aborted (pre-hooks) |
| 128+ | Fatal error | Operation aborted |
Git Command Patterns for Hooks
| Operation | Command | Use Case |
|---|---|---|
| List staged files | git diff --cached --name-only | Get files to validate |
| Get staged content | git show :path/to/file | Read staged version |
| Get commit message | git log -1 --pretty=%B | Read last commit |
| Get current branch | git symbolic-ref --short HEAD | Branch-specific logic |
| Check file status | git diff --cached --name-status | Detect added/modified/deleted |
| Get commit SHA | git rev-parse HEAD | Identify commits |
Ruby Execution Patterns
| Pattern | Code | Purpose |
|---|---|---|
| Run command | system(command) | Execute with inherited I/O |
| Capture output | %x(command) or backticks | Get stdout as string |
| Full control | Open3.capture3(command) | Get stdout, stderr, status |
| Check success | $?.success? | Verify last command status |
Environment Variables in Hooks
| Variable | Contains | Available In |
|---|---|---|
| GIT_DIR | Path to .git directory | All hooks |
| GIT_INDEX_FILE | Path to index file | Some hooks |
| GIT_AUTHOR_NAME | Commit author name | Commit hooks |
| GIT_AUTHOR_EMAIL | Commit author email | Commit hooks |
| GIT_EDITOR | Editor command | Interactive hooks |
File Permission Commands
| Command | Effect |
|---|---|
| chmod +x hook-file | Make hook executable |
| chmod -x hook-file | Disable hook |
| ls -l .git/hooks | Check permissions |
Hook Installation Script Template
#!/usr/bin/env ruby
require 'fileutils'
hooks_source = 'scripts/git-hooks'
hooks_target = '.git/hooks'
Dir["#{hooks_source}/*"].each do |source|
name = File.basename(source)
target = "#{hooks_target}/#{name}"
FileUtils.cp(source, target)
FileUtils.chmod(0755, target)
puts "Installed: #{name}"
end
Validation Check Examples
| Check Type | Implementation |
|---|---|
| Ruby syntax | ruby -c filename |
| RuboCop | bundle exec rubocop filename |
| RSpec | bundle exec rspec |
| File exists | File.exist?(path) |
| Pattern match | string.match?(/regex/) |
| Command available | system("which command") |