CrackedRuby CrackedRuby

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")