In May I shipped gitpulse — a tiny CLI that shows your git repo's analytics right in the terminal: commit stats, contributor graphs, file breakdowns, recent activity.
One command. Zero config. No dashboard. No login.
npx @wuchunjie/gitpulse
Three months later, I ran it on my own Windows machine and got:
❌ Not a git repository.
...inside a perfectly valid git repository. And the worst part: it had been failing on every Windows machine since day one. It just never told me.
The Bug
Here's the line that checked whether we're inside a git repo:
function run(cmd) {
try {
return execSync(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
} catch {
return "";
}
}
const isRepo = run(`cd "${dir}" && git rev-parse --git-dir 2>/dev/null`);
if (!isRepo) {
console.log(" ❌ Not a git repository.");
process.exit(1);
}
This is completely normal, boring code. git rev-parse --git-dir prints .git in a repo, nothing on stdout otherwise, and 2>/dev/null keeps the error message quiet. Works on Linux. Works on macOS. Works in my CI.
Then why did it die on Windows?
2>/dev/null Does Not Exist on Windows
On Linux and macOS, 2>/dev/null redirects stderr to the kernel's null device.
On Windows, Node's execSync doesn't run your command in bash — it runs it in cmd.exe. And cmd.exe interprets 2>/dev/null literally: "open the file at path /dev/null for writing."
That path doesn't exist. cmd.exe gives up with:
The system cannot find the path specified.
The command exits non-zero, my catch block swallows it, isRepo becomes an empty string, and gitpulse cheerfully announces "Not a git repository" in a repo that's very much a repo.
Silent failure. Wrong error message. Zero stack trace. For three months.
The Debug Journey
Because I develop in git-bash, the first clue was confusing: git rev-parse --git-dir worked fine in my shell, so of course I ran the same command through Node to compare:
node -e "console.log(require('child_process').execSync('git rev-parse --git-dir', {encoding:'utf-8'}).trim())"
# → .git ✅
Works in Node. Fails in gitpulse. So I bisected the exact command string, adding pieces back one by one:
execSync('cd "." && git rev-parse --git-dir') // ✅ works
execSync('cd "." && git rev-parse --git-dir 2>/dev/null') // ❌ boom
There it was. The redirect — the most innocent-looking six characters in the file — was the entire bug.
The Fix
One line: delete the redirect.
const isRepo = run(`cd "${dir}" && git rev-parse --git-dir`);
Why is that safe? Because I'm already passing stdio: ["pipe", "pipe", "pipe"] — stderr is piped into Node's memory and never touches the terminal. There is nothing for 2>/dev/null to silence. The redirect wasn't protecting the user from noise; it was just a habit I'd copied from a shell script.
That's the whole fix. No platform detection. No 2>nul on Windows. Just: don't put POSIX shell syntax inside a string you hand to child_process on Windows.
Rules I'm Writing Down Now
-
execSyncon Windows iscmd.exe, not bash. No2>/dev/null, no&&chains with POSIX paths, no$VARexpansion. -
Prefer Node options over shell tricks.
stdio: "ignore"or piped streams beat every shell redirect. -
gitcommands print errors to stderr and exit codes — you usually don't need to suppress anything if you handle the exit code properly. - Test on the OS your users are on. I write "cross-platform" in my README and then test on the one machine in my head. gitpulse users on Windows were getting a fake error for 90 days and I never noticed.
What gitpulse Actually Shows
The apology tour is over — here's the tool. Run it in any repo:
📊 GITPULSE
📝 Total commits: 2
👤 Contributors: 1
📅 Active days: 1
📂 Files touched: 2
👥 Top Contributors
T ███████████████ 2
📂 File Type Breakdown
.txt ███████████████ 2
🔥 Recent Activity
2026-08-30 ██████████████████████████████ 2
Point it at a big team repo and you get your bus-factor graph, the file-type mix (how much of your history is .ts vs .md vs mystery binary files), and a per-day activity strip — all rendered with plain Unicode blocks, no dependencies, no server, nothing leaving your machine.
It's the "wait, who actually maintains this repo?" answer, without opening a browser.
npx @wuchunjie/gitpulse # current repo
npx @wuchunjie/gitpulse /path/to/repo
If You're Shipping CLIs
Your users are on Windows. Not "some of your users." The ones who will install it, run it, get a confusing error, and quietly never come back.
Audit your execSync strings. If any of them contain 2>/dev/null, >/dev/null, or a bash-ism of any flavor, they are lying to your Windows users right now.
And if you ever build a tool that looks at your git history the way you'd wish GitHub Insights did — but faster, offline, and in your terminal — gitpulse is live on npm. npx @wuchunjie/gitpulse and tell me what your repo looks like. I read every comment.
Top comments (0)