Hello, I'm Shrijith Venkatramana. I'm building git-lrc, an AI code reviewer that runs on every commit. Star Us to help devs discover the project. Do give it a try and share your feedback for improving the product.
Claude Code hooks are powerful. They let you intercept tool execution, enforce policies, run validations, collect telemetry, or integrate external systems before and after Claude performs actions.
Unfortunately, the moment you try to distribute hooks to real developers, you run into a problem:
- Some developers use Linux
- Some use macOS
- Some use Windows PowerShell
- Some use Git-Bash
- Some use WSL
- Some use combinations of all of the above
A simple shell script quickly turns into a compatibility nightmare.
After experimenting with several approaches, I arrived at a surprisingly effective pattern:
Use thin platform-specific wrappers whose only job is downloading and launching a Go binary. Put all real logic inside the Go executable.
This gives you the convenience of native hooks while keeping the implementation portable, testable, and maintainable.
Let's walk through the architecture.
The Cross-Platform Hook Problem
Suppose you build a hook that validates commands before Claude executes them.
The naive implementation might look like this:
#!/usr/bin/env bash
python validate.py
Looks fine until:
- Python isn't installed
- The user runs PowerShell
- The user runs Git-Bash
- The user runs WSL
- Path handling differs
- Quoting rules differ
Now you have:
validate.sh
validate.ps1
validate.py
requirements.txt
And eventually:
works-on-my-machine/
The problem isn't Claude.
The problem is that shells are operating-system specific.
The Better Architecture
Instead, think of the hook as a bootstrapper.
Claude Hook
|
v
Thin Wrapper
|
v
Download Go Binary (if needed)
|
v
Execute Go Binary
|
v
Actual Hook Logic
The wrapper becomes extremely small.
The Go executable contains:
- Policy checks
- Configuration loading
- JSON parsing
- API calls
- Logging
- Cross-platform filesystem access
- Everything else
Once the binary exists locally, future hook invocations bypass installation entirely.
Bootstrapping on First Run
The wrapper checks whether the executable exists.
If not:
- Detect platform
- Download correct binary
- Make executable if needed
- Run binary
Example release layout:
releases/
├── hook-linux-amd64
├── hook-linux-arm64
├── hook-darwin-amd64
├── hook-darwin-arm64
├── hook-windows-amd64.exe
└── hook-windows-arm64.exe
A GitHub Releases page works perfectly for hosting.
Example Bash wrapper:
#!/usr/bin/env bash
set -e
HOOK_DIR="$HOME/.claude-hooks"
BIN="$HOOK_DIR/hook"
mkdir -p "$HOOK_DIR"
if [ ! -f "$BIN" ]; then
curl -L \
https://example.com/hook-linux-amd64 \
-o "$BIN"
chmod +x "$BIN"
fi
exec "$BIN" "$@"
The wrapper is tiny and almost never changes.
Supporting PowerShell
Windows users deserve first-class support.
PowerShell wrapper:
$HookDir = "$env:USERPROFILE\.claude-hooks"
$Binary = "$HookDir\hook.exe"
New-Item `
-ItemType Directory `
-Force `
-Path $HookDir | Out-Null
if (!(Test-Path $Binary)) {
Invoke-WebRequest `
-Uri "https://example.com/hook-windows-amd64.exe" `
-OutFile $Binary
}
& $Binary $args
exit $LASTEXITCODE
The important detail is that PowerShell's quoting rules differ significantly from Bash.
By moving all logic into Go, you avoid maintaining duplicate implementations.
Handling WSL and Git-Bash
This is where things become interesting.
Many Windows developers don't actually run PowerShell.
They run:
- WSL Ubuntu
- Git-Bash
- MSYS2
- Cygwin
Each environment reports itself differently.
A good Go bootstrapper can detect them.
Example:
func detectEnvironment() string {
if runtime.GOOS != "windows" {
return "native"
}
if os.Getenv("WSL_DISTRO_NAME") != "" {
return "wsl"
}
if os.Getenv("MSYSTEM") != "" {
return "git-bash"
}
return "powershell"
}
You can then adjust behavior.
For example:
switch detectEnvironment() {
case "wsl":
// Linux paths
case "git-bash":
// Mixed Windows/POSIX paths
case "powershell":
// Native Windows paths
}
This is dramatically easier than maintaining separate shell implementations.
Cross-Platform Techniques in Go
The Go standard library already solves most portability issues.
Use filepath
Avoid hardcoded separators.
Bad:
path := home + "/config/settings.json"
Good:
path := filepath.Join(
home,
"config",
"settings.json",
)
Use os.UserHomeDir
Avoid platform assumptions.
home, err := os.UserHomeDir()
Works on:
- Linux
- macOS
- Windows
- WSL
Use os.Executable
Finding your own binary location:
exe, err := os.Executable()
Useful when loading bundled resources.
Detect Operating System
switch runtime.GOOS {
case "windows":
// Windows
case "linux":
// Linux
case "darwin":
// macOS
}
Detect Architecture
fmt.Println(runtime.GOARCH)
Possible values:
amd64
arm64
386
Useful for selecting downloads.
Full Example Downloader
A minimal self-updating launcher:
package main
import (
"io"
"net/http"
"os"
)
func download(url string, dest string) error {
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
out, err := os.Create(dest)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, resp.Body)
return err
}
Combined with:
runtime.GOOS
runtime.GOARCH
you can dynamically fetch the correct binary.
Why This Pattern Scales Better
The biggest benefit isn't portability.
It's maintainability.
Without this pattern:
hook.sh
hook.ps1
hook.py
hook.js
With this pattern:
hook.sh (tiny)
hook.ps1 (tiny)
hook-go/
all logic
The wrappers rarely change.
The Go binary evolves independently.
Testing becomes easier.
Distribution becomes easier.
Versioning becomes easier.
And most importantly, you stop fighting shell differences.
Final Thoughts
Many engineering teams start by writing Claude hooks as shell scripts because it feels fast.
That works for one machine.
The moment multiple operating systems enter the picture, the complexity grows rapidly.
A small bootstrap wrapper plus a Go executable gives you a surprisingly robust deployment model:
- Bash support
- PowerShell support
- Linux support
- macOS support
- Windows support
- WSL support
- Git-Bash support
- Single implementation of business logic
The shell becomes a launcher.
Go becomes the platform.
That's usually the point where hook maintenance stops being a headache.
How are you handling cross-platform automation today—shell scripts, Node.js, Python, or compiled binaries? I'd be interested to hear which approach has held up best as your team and environments grew.
*AI agents write code fast. They also silently remove logic, change behavior, and introduce bugs -- without telling you. You often find out in production.
git-lrc fixes this. It hooks into git commit and reviews every diff before it lands. 60-second setup. Completely free.*
Any feedback or contributors are welcome! It's online, source-available, and ready for anyone to use.
HexmosTech
/
git-lrc
Free, Micro AI Code Reviews That Run on Commit
| 🇩🇰 Dansk | 🇪🇸 Español | 🇮🇷 Farsi | 🇫🇮 Suomi | 🇯🇵 日本語 | 🇳🇴 Norsk | 🇵🇹 Português | 🇷🇺 Русский | 🇦🇱 Shqip | 🇨🇳 中文 |
git-lrc
Free, Micro AI Code Reviews That Run on Commit
AI agents write code fast. They also silently remove logic, change behavior, and introduce bugs -- without telling you. You often find out in production.
git-lrc fixes this. It hooks into git commit and reviews every diff before it lands. 60-second setup. Completely free.
See It In Action
See git-lrc catch serious security issues such as leaked credentials, expensive cloud operations, and sensitive material in log statements
git-lrc-intro-60s.mp4
Why
- 🤖 AI agents silently break things. Code removed. Logic changed. Edge cases gone. You won't notice until production.
- 🔍 Catch it before it ships. AI-powered inline comments show you exactly what changed and what looks wrong.
- 🔁 Build a…
Top comments (0)