| Crates.io | fasthooks |
| lib.rs | fasthooks |
| version | 0.1.0 |
| created_at | 2025-12-06 09:05:27.38334+00 |
| updated_at | 2025-12-06 09:05:27.38334+00 |
| description | Blazing fast Git hooks manager written in Rust. Drop-in replacement for Husky with 27x faster startup. |
| homepage | https://github.com/alfredo-baratta/fasthooks |
| repository | https://github.com/alfredo-baratta/fasthooks |
| max_upload_size | |
| id | 1969854 |
| size | 229,602 |
Blazing fast Git hooks manager written in Rust. Drop-in replacement for Husky with 27x faster startup.
→ Running pre-commit hook...
✓ eslint (3263ms)
✓ prettier (2045ms)
✓ typecheck (2507ms)
✓ 3 tasks passed
⏱ Completed in 3.26s
⚡ Saved 4.55s through parallelization
🌱 Saved ~0.11g CO₂ vs Node.js-based tools
| Feature | FastHooks | Husky + lint-staged |
|---|---|---|
| Startup time | ~12ms | ~1400ms |
| Speedup | 27x faster | baseline |
| Language | Rust (native binary) | Node.js |
| Parallel execution | Built-in | Requires configuration |
| Dependencies | Zero runtime deps | Node.js ecosystem |
| Configuration | TOML (simple) | Multiple files |
| Carbon tracking | ✅ Built-in | ❌ |
# Using cargo
cargo install fasthooks
# Using Homebrew (macOS/Linux)
brew install fasthooks
# Download pre-built binary (Windows/Linux/macOS)
# See releases page for latest binaries
# Create configuration and install hooks
fasthooks init
# Or migrate from Husky
fasthooks migrate
Create a fasthooks.toml in your project root:
version = "1"
[settings]
parallel = true
fail_fast = true
show_stats = true
[hooks.pre-commit]
tasks = [
{ name = "lint", run = "npm run lint", glob = "*.{js,ts}" },
{ name = "format", run = "npm run format", glob = "*.{js,ts,json}" },
{ name = "test", run = "npm test" }
]
[hooks.pre-push]
tasks = [
{ name = "build", run = "npm run build" }
]
| Command | Description |
|---|---|
fasthooks init |
Initialize FastHooks in the current repository |
fasthooks install |
Install Git hooks based on configuration |
fasthooks uninstall |
Remove all FastHooks Git hooks |
fasthooks run <hook> |
Manually run a specific hook |
fasthooks add <hook> <cmd> |
Add a command to a hook |
fasthooks list |
List all configured hooks |
fasthooks validate |
Validate configuration file |
fasthooks migrate |
Migrate from Husky to FastHooks |
fasthooks benchmark |
Compare performance with Husky |
# Run pre-commit hook manually
fasthooks run pre-commit
# Run with specific files
fasthooks run pre-commit --files src/main.rs --files src/lib.rs
# Run commit-msg hook with argument (commit message file path)
fasthooks run commit-msg -- .git/COMMIT_EDITMSG
# Run pre-push hook with arguments (remote name and URL)
fasthooks run pre-push -- origin https://github.com/user/repo.git
# Validate configuration
fasthooks validate
version = "1"
[settings]
# Run tasks in parallel (default: true)
parallel = true
# Maximum parallel tasks, 0 = auto-detect CPU cores (default: 0)
max_parallel = 0
# Stop on first failure (default: true)
fail_fast = true
# Show execution statistics (default: true)
show_stats = true
# Show carbon savings estimate (default: true)
show_carbon_savings = true
# Skip all hooks in CI environment (default: false)
skip_ci = false
# Enable colored output (default: true)
colors = true
[hooks.pre-commit]
# Override global parallel setting for this hook
parallel = true
# Override global fail_fast setting for this hook
fail_fast = true
# Skip this hook in CI environment
skip_ci = false
# Tasks to run
tasks = [
# ... task definitions
]
Each task supports the following options:
[hooks.pre-commit]
tasks = [
{
# Required: Task name (for display)
name = "lint",
# Required: Command to execute
run = "npm run lint",
# Optional: Glob pattern to filter files (supports exclusions with !)
glob = "*.{js,ts}, !*.test.js",
# Optional: Only run on staged files (default: true)
staged = true,
# Optional: Working directory for the command
cwd = "./packages/app",
# Optional: Additional environment variables
env = { NODE_ENV = "test" },
# Optional: Continue even if this task fails (default: false)
allow_failure = false,
# Optional: Condition to run this task
if = "branch != main",
# Optional: Tasks that must run before this one
depends_on = ["format", "typecheck"]
}
]
Filter files using glob patterns. Use ! prefix to exclude files:
[hooks.pre-commit]
tasks = [
# Match all .rs files except those in tests/
{ name = "clippy", run = "cargo clippy", glob = "*.rs, !tests/*.rs" },
# Match JS/TS files except test and config files
{ name = "lint", run = "eslint", glob = "*.{js,ts}, !*.test.js, !*.config.js" },
# Multiple patterns separated by comma or space
{ name = "format", run = "prettier --write", glob = "*.json *.md *.yml" }
]
How it works:
!pattern excludes files matching that patternExamples:
| Pattern | Matches | Doesn't Match |
|---|---|---|
*.rs |
main.rs, src/lib.rs |
test.js |
*.rs, !tests/*.rs |
src/main.rs |
tests/test.rs |
src/**/*.ts |
src/app/index.ts |
lib/index.ts |
Define execution order with depends_on:
[hooks.pre-commit]
tasks = [
{ name = "install", run = "npm install" },
# These run after install completes
{ name = "lint", run = "npm run lint", depends_on = ["install"] },
{ name = "format", run = "npm run format", depends_on = ["install"] },
# This runs after both lint and format complete
{ name = "test", run = "npm test", depends_on = ["lint", "format"] }
]
Execution order:
install
├── lint ──┐
└── format ┴── test
Features:
parallel = true)Run tasks only when conditions are met using the if field:
[hooks.pre-commit]
tasks = [
# Only run on main branch
{ name = "deploy-check", run = "npm run deploy:check", if = "branch == main" },
# Skip on main branch (run on all other branches)
{ name = "dev-lint", run = "npm run lint:dev", if = "branch != main" },
# Works with any branch name
{ name = "feature-test", run = "npm test", if = "branch == feature/new-ui" }
]
[hooks.pre-commit]
tasks = [
# Only run when CI environment variable is set
{ name = "ci-tests", run = "npm run test:ci", if = "env:CI" },
# Only run when NOT in CI (local development)
{ name = "local-lint", run = "npm run lint:fix", if = "!env:CI" },
# Check for any environment variable
{ name = "debug", run = "npm run debug", if = "env:DEBUG" }
]
[hooks.pre-commit]
tasks = [
# Only run if package-lock.json exists
{ name = "npm-audit", run = "npm audit", if = "exists:package-lock.json" },
# Only run if .env file doesn't exist (prevent committing secrets)
{ name = "check-env", run = "echo 'Safe to commit'", if = "!exists:.env.local" },
# Check for configuration file
{ name = "eslint", run = "eslint .", if = "exists:.eslintrc.js" }
]
| Condition | Description | Example |
|---|---|---|
branch == <name> |
Current branch equals name | if = "branch == main" |
branch != <name> |
Current branch not equals name | if = "branch != develop" |
env:<VAR> |
Environment variable is set | if = "env:CI" |
!env:<VAR> |
Environment variable is not set | if = "!env:DEBUG" |
exists:<path> |
File or directory exists | if = "exists:package.json" |
!exists:<path> |
File or directory doesn't exist | if = "!exists:.secrets" |
Git passes arguments to certain hooks. Access them with $1, $2, etc.:
[hooks.commit-msg]
tasks = [
# $1 = path to commit message file
{ name = "lint-commit", run = "commitlint --edit $1" },
{ name = "check-msg", run = "cat $1 | grep -E '^(feat|fix|docs):'" }
]
[hooks.pre-push]
tasks = [
# $1 = remote name, $2 = remote URL
{ name = "check-remote", run = "echo Pushing to $1 at $2" }
]
[hooks.prepare-commit-msg]
tasks = [
# $1 = commit msg file, $2 = source, $3 = SHA
{ name = "add-branch", run = "echo 'Branch:' $(git branch --show-current) >> $1" }
]
Alternative syntax: Use {1}, {2} instead of $1, $2:
{ name = "lint-commit", run = "commitlint --edit {1}" }
Hook Arguments Reference:
| Hook | Arguments |
|---|---|
pre-commit |
(none) |
prepare-commit-msg |
$1 = commit msg file, $2 = source, $3 = SHA |
commit-msg |
$1 = commit msg file |
post-commit |
(none) |
pre-push |
$1 = remote name, $2 = remote URL |
pre-rebase |
$1 = upstream, $2 = rebased branch |
post-checkout |
$1 = prev HEAD, $2 = new HEAD, $3 = is branch checkout |
post-merge |
$1 = is squash merge |
By default, tasks run in parallel to maximize performance:
[settings]
parallel = true # Enable parallel execution globally
max_parallel = 4 # Limit to 4 concurrent tasks (0 = auto-detect)
[hooks.pre-commit]
parallel = true # Override for this hook
tasks = [
# These three run simultaneously
{ name = "eslint", run = "eslint .", glob = "*.{js,ts}" },
{ name = "prettier", run = "prettier --check .", glob = "*.{json,md}" },
{ name = "stylelint", run = "stylelint '**/*.css'", glob = "*.css" }
]
Sequential execution: Set parallel = false or use depends_on:
[hooks.pre-commit]
parallel = false # Tasks run one after another
tasks = [
{ name = "first", run = "echo 1" },
{ name = "second", run = "echo 2" },
{ name = "third", run = "echo 3" }
]
Control behavior when a task fails:
[settings]
fail_fast = true # Stop immediately on first failure (default)
[hooks.pre-commit]
fail_fast = false # Override: continue running other tasks even if one fails
tasks = [
{ name = "lint", run = "npm run lint" },
{ name = "test", run = "npm test" },
# This task continues even if it fails
{ name = "optional-check", run = "npm run check", allow_failure = true }
]
Behavior comparison:
| Setting | On Failure |
|---|---|
fail_fast = true |
Stop execution, skip remaining tasks |
fail_fast = false |
Continue running all tasks |
allow_failure = true |
Task failure doesn't affect overall result |
FastHooks can automatically migrate your existing Husky + lint-staged configuration:
fasthooks migrate
This will:
.husky/ directorypackage.json or .lintstagedrcfasthooks.tomlBefore (Husky + lint-staged):
.husky/pre-commit:
#!/bin/sh
npx lint-staged
package.json:
{
"lint-staged": {
"*.{js,ts}": ["eslint --fix", "prettier --write"],
"*.css": "stylelint --fix"
}
}
After (FastHooks):
fasthooks.toml:
version = "1"
[settings]
parallel = true
fail_fast = true
[hooks.pre-commit]
tasks = [
{ name = "eslint", run = "eslint --fix {files}", glob = "*.{js,ts}", staged = true },
{ name = "prettier", run = "prettier --write {files}", glob = "*.{js,ts}", staged = true },
{ name = "stylelint", run = "stylelint --fix {files}", glob = "*.css", staged = true }
]
Validate your configuration to catch errors early:
fasthooks validate
Example output for valid config:
→ Validating configuration...
Config file: /path/to/fasthooks.toml
✓ Configuration is valid!
Summary:
• Version: 1
• Hooks configured: 2
→ pre-commit (3 tasks)
• lint [glob: *.{js,ts}]
• format [depends_on: ["lint"]]
• test [if: branch != main]
→ pre-push (1 task)
• build
• Settings:
Parallel: yes, Fail fast: yes, Show stats: yes
Example output for invalid config:
→ Validating configuration...
✗ Found 2 validation error(s):
1. Task 'build' depends on 'compile' which doesn't exist
Location: hooks.pre-commit.tasks[2]
Suggestion: Either add a task named 'compile' or remove it from depends_on
2. Duplicate task name: 'lint'
Location: hooks.pre-commit.tasks[3]
Suggestion: Each task must have a unique name within a hook
Real-world benchmark on a JavaScript/TypeScript project with ESLint, Prettier, and TypeScript:
| Metric | FastHooks | Husky (via npx) | Speedup |
|---|---|---|---|
| Startup time | 12ms | 1400ms | 117x |
| Hook overhead | 52ms | 1400ms | 27x |
| Parallel execution | Built-in | Manual setup | - |
Run your own benchmark:
fasthooks benchmark
FastHooks estimates the environmental impact of using native tooling:
🌱 Saved ~0.02g CO₂ vs Node.js-based tools
Calculation based on:
FastHooks supports all standard Git hooks:
| Hook | When it runs |
|---|---|
pre-commit |
Before commit is created |
prepare-commit-msg |
After default message, before editor |
commit-msg |
After commit message is entered |
post-commit |
After commit is created |
pre-push |
Before push to remote |
pre-rebase |
Before rebase starts |
post-checkout |
After checkout completes |
post-merge |
After merge completes |
pre-auto-gc |
Before automatic garbage collection |
version = "1"
[settings]
parallel = true
max_parallel = 0
fail_fast = true
show_stats = true
show_carbon_savings = true
skip_ci = false
colors = true
[hooks.pre-commit]
parallel = true
fail_fast = true
tasks = [
# Format code first
{ name = "prettier", run = "prettier --write {files}", glob = "*.{js,ts,json,md}", staged = true },
# Lint after formatting
{ name = "eslint", run = "eslint --fix {files}", glob = "*.{js,ts}", staged = true, depends_on = ["prettier"] },
# Type check (no file filter needed)
{ name = "typecheck", run = "tsc --noEmit", depends_on = ["prettier"] },
# Run tests only on feature branches
{ name = "test", run = "npm test", if = "branch != main", depends_on = ["eslint", "typecheck"] },
# Security audit in CI only
{ name = "audit", run = "npm audit", if = "env:CI", allow_failure = true }
]
[hooks.commit-msg]
tasks = [
# Validate commit message format
{ name = "commitlint", run = "commitlint --edit $1" }
]
[hooks.pre-push]
tasks = [
# Full test suite before push
{ name = "test-all", run = "npm run test:all" },
# Build check
{ name = "build", run = "npm run build", depends_on = ["test-all"] }
]
Contributions are welcome! Please read CONTRIBUTING.md for guidelines.
MIT License - see LICENSE for details.
Built with 🦀 Rust for a greener future