Skip to main content

← Blog

Secret Scanning in a Frontend Repo: Gitleaks in Husky and CI

A practical Gitleaks setup for frontend repositories using Husky pre-commit hooks and GitHub Actions, with staged redaction and full-repository CI scanning.

Published
September 12, 2025
Updated
Updated July 12, 2026
Author
Daniel Mark
Reading time
6 min read

While building the enterprise front-end boilerplate at AxLabs GmbH, I wired Gitleaks into Husky pre-commit and GitHub Actions. The goal was simple: catch likely secrets before push, then scan the full working tree in CI when a hook is skipped or bypassed.

Frontend repos are not immune to committed secrets. A Next.js starter still needs JWT_SECRET, database URLs, and third-party API keys in server env. Type-safe env validation with @t3-oss/env-nextjs catches missing variables at runtime. It does not catch a Stripe test key sitting in a staged JSON file.

This is concrete DX hygiene, not a substitute for rotation, vaults, or server-side enforcement.

Why a frontend repo needs this

The boilerplate's server env schema validates DATABASE_URLJWT_SECRET, and INTERNAL_API_KEY through Zod at startup. That is the right layer for "is this app configured?" It is the wrong layer for "did someone just commit a key?"

Frontend engineers own the Husky hook and the CI workflow files alongside ESLint and Jest. Secret scanning belongs in that same toolchain. Many starter templates stop at lint-staged. Adding Gitleaks is a small diff with a clear failure mode.

Local layer: staged scan before lint-staged

The pre-commit hook in enterprise-front-end-boilerplate runs Gitleaks first, then lint-staged. If Gitleaks fails, the commit aborts and Prettier never runs.

1
echo "Running Gitleaks on staged files..."
2
3
MSYS_NO_PATHCONV=1 docker run --rm --network=none \
4
-v "$(pwd):/repo:ro" -w /repo \
5
ghcr.io/gitleaks/gitleaks:8 \
6
git --pre-commit --redact --staged --verbose
7
8
if [ $? -ne 0 ]; then
9
echo "Gitleaks found secrets. Commit aborted."
10
exit 1
11
fi
12
13
echo "Gitleaks passed. Running lint-staged..."
14
npx lint-staged

Three flags matter for day-to-day DX:

--staged limits the scan to what you are about to commit. Unstaged junk in your working tree does not block a clean commit.

--redact masks matched secret values in terminal output. When Gitleaks fires on a real key, you do not want the full value printed to a screen-share or CI log.

git --pre-commit tells Gitleaks to evaluate staged content the way a pre-commit hook should.

MSYS_NO_PATHCONV=1 is a Git Bash on Windows fix for Docker volume mounts. On WSL or native Linux, the flag is harmless.

Order is deliberate. Formatting a file that contains a secret does not help. Scan first, format second.

CI layer: full-tree scan on push and PR

The GitHub Actions workflow mounts the checkout and runs detect --no-git against the entire tree:

1
- name: Run Gitleaks in Docker (fail on secrets)
2
run: |
3
docker run --rm --network=none \
4
-v "$PWD:/repo:ro" -w /repo \
5
ghcr.io/gitleaks/gitleaks:8 \
6
detect --no-git --source=/repo \
7
--redact --verbose --exit-code 1

--no-git scans files on disk at the checked-out ref. It catches secrets anywhere in the repo, including files you never staged locally.

What it does not do: walk full git history for secrets committed months ago and later deleted. Historical leak detection needs gitleaks detect without --no-git, or a dedicated secrets scanning integration on the remote. Our CI choice optimizes for "nothing in the current tree" rather than forensic history review. That is a scope limit worth stating plainly.

Gitleaks sits alongside Jest, Next.js build, ESLint, and Prettier workflows. Peer infrastructure, not a security sidebar.

Why Docker with --network=none

Both hook and CI invoke Gitleaks inside Docker with --network=none, a read-only bind mount, and an explicit image version.

Running Gitleaks through Docker avoids installing a separate binary on every machine. Read-only mounts mean the scanner cannot mutate your working tree. --network=none keeps the scan offline. Pinning the image version keeps the scanner behavior reproducible across local hooks and CI instead of letting an upstream release change rules without a repo diff.

The Docker requirement matters enough to document directly. If the pre-commit hook depends on a running Docker daemon, onboarding should say that plainly. Otherwise, a new contributor finds out through a failed commit instead of the setup guide.

What this pattern does not solve

Gitleaks in Husky and CI is a detection net, not a security program.

Rotation and revocation. Finding a key in a commit does not rotate it.

Secret storage. Keys still live in .env.local on developer machines.

Server-side enforcement. A leaked NEXT_PUBLIC_ value is public by definition.

Hook bypass. git commit --no-verify skips Husky. CI is the backstop.

False positives. Test fixtures and fake keys in demos can trip rules. You may need a .gitleaks.toml allowlist as the codebase grows.

Comprehensive testing. The repo lists Playwright as a devDependency and the README says it is "configured." There is no playwright.config.*, no e2e specs, and no Playwright CI job. Secret scanning does not compensate for that gap.

Why this pattern is portable

The reason I like this setup is that it does not depend on the app framework, the component library, or the product domain. Any frontend repo with environment variables, CI, and pre-commit hooks can adopt the same local-plus-CI split.

That portability is what makes it a useful boilerplate pattern. It catches a common class of mistakes without requiring a larger platform, a secret manager rollout, or a new deployment model.

Secret scanning was one of the few boilerplate patterns that felt file-backed and copyable from the start. Other platform governance concerns can come later. A small pre-commit hook and a CI job already reduce a very common class of mistakes.

Tradeoffs I would accept again

Staged local scan plus full-tree CI is the right split. Local stays fast and relevant to the commit you are making. CI catches unstaged leaks, hook skips, and files outside src/ that lint-staged never touches.

Docker as the distribution mechanism trades a hard dependency for consistency. A Homebrew install is lighter for contributors who refuse Docker, but you lose identical versions across macOS, Linux, and CI without more scripting.

A pinned scanner version is slightly more maintenance, but worth it. Security tooling should not quietly change behavior because a remote image tag moved. The cost is remembering to bump the version deliberately when you want new Gitleaks rules or fixes.

What I would improve next

  1. Add a .gitleaks.toml with project-specific allowlists once false positives appear.
  2. Decide on history scanning if you need to catch removed secrets still in git history.
  3. Add a small contributor note explaining common Docker daemon failures on macOS, Linux, and Windows.
  4. Align CI Node and pnpm versions with package.json while editing workflows.
  5. Add an explicit security checklist for what to do when Gitleaks finds a real secret.

None of these block the core pattern. They are the gaps I would close before presenting the setup as production-ready.

Closing

Frontend repos accumulate secrets in env files, test data, and accidental commits. A dual Gitleaks setup (staged pre-commit with redaction, full-tree CI with --no-git) catches most day-to-day mistakes without pretending to be enterprise-grade security on its own. Wire it next to the hooks you already maintain, document the Docker requirement honestly, and treat CI as the backstop when someone uses --no-verify.

I would copy the pattern again, but I would also keep the scanner version pinned and the Docker prerequisite visible in onboarding so the setup stays predictable for the next person using it.


Portrait of Daniel Mark

Written by

Daniel Mark

Senior Frontend Engineer

Daniel Mark is a senior frontend engineer and product consultant with 7+ years of experience building resilient product interfaces for Web3, SaaS, and enterprise teams. He writes about frontend architecture, design systems, performance, SEO, and the engineering decisions that turn complex systems into dependable user experiences.