When you build a product whose entire reason to exist is safety, security can't be something you bolt on later. It has to be a default — baked into the workflow from day one.
So before any application code, I set up how my app handles secrets. This post walks through that setup: a deliberate, three-layer approach that makes it structurally impossible for a token to end up in version control.
The star of the show is a Git pre-commit hook. I'll explain it from scratch.
Defense in depth
No single control should be the only thing standing between you and a leak. Three layers, each catching what the previous one might miss:
-
.gitignore— prevention: keep secret-bearing files out of Git entirely -
A
pre-commithook — detection: scan every commit for secrets and block it if one slips through - Environment variables — design: keep secrets out of the codebase in the first place
Layer 1 — .gitignore
# Environment variables
.env
.env.*
!.env.example
The !.env.example exception keeps a template in the repo — a documented list of which variables are needed, with empty values. Anyone picking up the project knows exactly what to fill in without ever seeing a secret.
Layer 2 — the pre-commit hook
A Git hook is a script Git runs automatically at a specific moment. A pre-commit hook runs right before a commit is created:
git commit
│
▼
pre-commit hook runs ← automatic
│
├─ secret found? → ❌ commit blocked
└─ all clean? → ✅ commit proceeds
Mine scans staged files for patterns that match real credentials — Atlassian tokens, AWS keys, private keys. If it finds one, the commit is blocked:
#!/usr/bin/env bash
set -euo pipefail
files=$(git diff --cached --name-only --diff-filter=ACM)
[ -z "$files" ] && exit 0
patterns='ATATT[A-Za-z0-9_=-]{16,}|AKIA[0-9A-Z]{16}|gh[pousr]_[A-Za-z0-9]{20,}|-----BEGIN [A-Z ]*PRIVATE KEY-----'
found=0
while IFS= read -r f; do
[ -n "$f" ] || continue
content=$(git show ":$f" 2>/dev/null || true)
if printf '%s' "$content" | grep -nEq "$patterns"; then
echo "❌ Possible secret in: $f"
found=1
fi
done <<< "$files"
if [ "$found" -ne 0 ]; then
echo "🛑 Commit blocked: secrets detected in staged files."
exit 1
fi
Two things worth knowing:
The exit code is everything. If a pre-commit hook exits non-zero, Git aborts the commit. That single exit 1 is what makes this real.
I keep the hook in the repo. Git's default hooks live in .git/hooks/, which isn't versioned. I store mine in a tracked .githooks/ folder and point Git at it:
git config core.hooksPath .githooks
Now the hook travels with the project and gets reviewed like any other code.
Layer 3 — environment variables
The first two layers stop secrets from being committed. The third makes sure they're not in the code to begin with:
const CONFIG = {
apiToken: process.env.JIRA_API_TOKEN,
}
if (!CONFIG.apiToken) {
console.error("Missing JIRA_API_TOKEN. Run with: node --env-file=.env.local script.js")
process.exit(1)
}
On Node 20.6+, no dotenv dependency needed — the built-in --env-file flag loads your .env.local:
node --env-file=.env.local script.js
Testing it
The best way to trust a safety net is to test it:
echo 'const token = "ATATT3xFAKEFAKEFAKEFAKEFAKE123456"' > leak-test.js
git add leak-test.js
git commit -m "test"
# ❌ Possible secret in: leak-test.js
# 🛑 Commit blocked: secrets detected in staged files.
The commit never happens. That's the point.
Takeaway
Security works best when the tooling enforces the rules — not your memory. Three small pieces of configuration and a whole category of mistakes simply can't happen.
For HandyFEM, where trust is the product, this wasn't over-engineering. It was the starting line.
📚 HandyFEM App Series
🔗 Previous: From Specs to Tickets: Automating Jira Setup with Node.js and the Jira API
🔗 Next: none (latest post)
🏷️ All posts in this series: #HandyFEMApp
*Follow the build: #HandyFEMApp *
Top comments (0)