Add gitleaks pre-commit hook, global gitignore, plaintext credential detection, SSH key hygiene audit, 8 new git config settings, and safe.directory wildcard detection. Fix ssh-keygen macOS compatibility, FIDO2 detection via ioreg, and interactive test isolation. Implements docs/specs/2026-03-31-v0.2.0-expanded-hardening.md Co-Authored-By: Claude <noreply@anthropic.com>
13 KiB
git-harden.sh v0.2.0 — Expanded Hardening Features
Motivation
Gap analysis of two independent research reports (Claude Opus 4.6 and Gemini 3.1 Pro, March 2026) against the v0.1.0 script identified six feature areas where the script falls short of current best-practice recommendations. All additions follow the existing audit+apply pattern and require no new CLI flags.
Source Reports
docs/research/Claude Opus 4.6 report.mddocs/research/Gemini 3.1 Pro report.md
Scope
All changes are additive — no existing behavior changes. The v0.2.0 script will:
- Install a gitleaks pre-commit hook
- Create and configure a global gitignore
- Detect plaintext credential files
- Audit SSH key hygiene
- Add 8 new git config settings
- Detect dangerous
safe.directory = *wildcard
Version bump: 0.1.0 → 0.2.0.
Feature 1: Pre-commit Hook Installation (gitleaks)
Background
Both reports rank pre-commit secret scanning as the single most impactful workstation-level defense. The v0.1.0 script sets core.hooksPath = ~/.config/git/hooks but installs no hooks, leaving the directory empty.
Audit Behavior
- Check if
~/.config/git/hooks/pre-commitexists and is executable. - If it exists, check whether it contains a
gitleaksinvocation (grep forgitleaks). [OK]if pre-commit hook exists and references gitleaks.[WARN]if pre-commit hook exists but does NOT reference gitleaks (user-managed hook — don't touch).[MISS]if no pre-commit hook exists.
Apply Behavior
- Check if
gitleaksis on$PATHviacommand -v gitleaks. - If gitleaks is found and no pre-commit hook exists:
- Create
~/.config/git/hooks/pre-commitwith the following content:#!/usr/bin/env bash # Installed by git-harden.sh — global pre-commit secret scanning # To bypass for a single commit: SKIP_GITLEAKS=1 git commit set -o errexit set -o nounset set -o pipefail if [ "${SKIP_GITLEAKS:-0}" = "1" ]; then exit 0 fi if command -v gitleaks >/dev/null 2>&1; then gitleaks protect --staged --redact --verbose fi chmod +xthe hook.
- Create
- If gitleaks is NOT found:
[WARN]with install instructions:- macOS:
brew install gitleaks - Linux:
brew install gitleaksor download from GitHub releases
- macOS:
- Still create the hook script (it guards with
command -vso it's safe without gitleaks installed). Prompt the user before creating.
- If a pre-commit hook already exists (any content): warn and skip. Do not overwrite user-managed hooks.
Bypass Mechanism
The SKIP_GITLEAKS=1 environment variable allows a single commit to bypass the hook without --no-verify (which skips ALL hooks). This is documented in the hook script itself.
Acceptance Criteria
--auditreports status of pre-commit hook with gitleaks.- Apply creates a working hook that blocks commits containing secrets.
- Existing user hooks are never overwritten.
- Hook is safe to install even if gitleaks is not yet installed.
Feature 2: Global Gitignore
Background
Both reports stress maintaining comprehensive .gitignore patterns for secrets. No amount of scanning catches what was never tracked in the first place.
Audit Behavior
- Check if
core.excludesFileis set in global git config. - If not set:
[MISS]. - If set: check whether the referenced file contains key security patterns (
.env,*.pem,*.key).[OK]if file exists and contains at least one security pattern.[WARN]if file exists but lacks security patterns: "Global gitignore at lacks secret patterns (.env, *.pem, *.key). Consider adding them."
Apply Behavior
- If
core.excludesFileis not set:- Create
~/.config/git/ignorewith the following patterns:# === Security: secrets & credentials === .env .env.* !.env.example *.pem *.key *.p12 *.pfx *.jks credentials.json service-account*.json .git-credentials .netrc .npmrc .pypirc # === Security: Terraform state (contains secrets) === *.tfstate *.tfstate.backup # === OS artifacts === .DS_Store Thumbs.db Desktop.ini # === IDE artifacts === .idea/ .vscode/ *.swp *.swo *~ - Set
core.excludesFile = ~/.config/git/ignoreviaapply_git_setting.
- Create
- If
core.excludesFileis already set to a different path:- Print
[INFO]noting the existing path. Do not modify or overwrite. - If the file lacks security patterns, print
[WARN]with the missing patterns (informational only — no auto-append).
- Print
Acceptance Criteria
- Audit reports whether
core.excludesFileis configured. - Audit checks existing gitignore files for security pattern coverage and warns if missing.
- Apply creates the file and sets the config only when nothing is configured.
- Existing configurations are never modified — warnings are informational.
- The
!.env.examplenegation allows committing example env files.
Feature 3: Plaintext Credential File Detection
Background
Both reports warn that git-credential-store writes passwords to ~/.git-credentials in plaintext. The Gemini report additionally calls out infostealer malware targeting this file. Adjacent credential files (.netrc, .npmrc, .pypirc) pose similar risks.
Audit Behavior (audit-only, no apply action)
Check for existence and content of these files:
| File | Detection | Severity |
|---|---|---|
~/.git-credentials |
File exists | [WARN] — "Plaintext git credentials. Migrate to credential helper (osxkeychain/libsecret) and delete this file." |
~/.netrc |
File exists | [WARN] — "Plaintext network credentials found. May contain git hosting tokens." |
~/.npmrc |
File contains _authToken= followed by a non-empty value (regex: _authToken=.+) |
[WARN] — "npm registry token in plaintext. Use npm config set with env vars instead." |
~/.pypirc |
File contains password |
[WARN] — "PyPI credentials in plaintext. Use keyring or token-based auth instead." |
Apply Behavior
None. The script does not delete or modify user credential files. Warnings are informational only.
Section Placement
New section: "Credential Hygiene" — placed after the existing "Credential Storage" audit.
Acceptance Criteria
- Each detected file produces a specific, actionable warning.
- No false positives on files that don't contain credentials (e.g., .npmrc with only registry URL, no token).
- No files are modified or deleted.
Feature 4: SSH Key Hygiene Audit
Background
Both reports recommend ed25519 exclusively. The Claude report notes GitHub blocks legacy RSA/SHA-1 signatures. The Gemini report recommends banning DSA and ECDSA.
Audit Behavior (audit-only, no apply action)
- Scan
~/.ssh/*.pubfiles. - Additionally, parse
IdentityFiledirectives from~/.ssh/config(the v0.1.0 script already has this parsing logic indetect_existing_keys) and include any referenced.pubfiles not already covered by the glob. - For each
.pubfile, read the first field to determine key type. - Use
ssh-keygen -l -f <file>to extract bit length for RSA keys. - Report:
| Key Type | Verdict |
|---|---|
ssh-ed25519 |
[OK] |
sk-ssh-ed25519@openssh.com |
[OK] |
ssh-rsa with >= 2048 bits |
[WARN] — "RSA key (%d bits). Consider migrating to ed25519." |
ssh-rsa with < 2048 bits |
[WARN] — "Weak RSA key (%d bits). Migrate to ed25519 immediately." |
ssh-dss |
[WARN] — "DSA key (deprecated). Migrate to ed25519." |
ecdsa-sha2-* |
[WARN] — "ECDSA key. Consider migrating to ed25519." |
sk-ecdsa-sha2-* |
[OK] — Hardware-backed ECDSA is acceptable. |
Apply Behavior
None. Key migration is too risky to automate. Warnings are informational.
Section Placement
New section: "SSH Key Hygiene" — placed after the existing "SSH Configuration" audit.
Acceptance Criteria
- All
.pubfiles in~/.ssh/are scanned and classified. - RSA bit length is correctly extracted.
- No false warnings on ed25519 or ed25519-sk keys.
- No keys are modified or deleted.
Feature 5: Additional Git Config Settings
Background
Eight new settings recommended by one or both reports that the v0.1.0 script does not audit or apply.
Settings
| Setting | Value | Rationale | Report Source | Section |
|---|---|---|---|---|
user.useConfigOnly |
true |
Prevent commits without explicit identity — forces user.name/email to be set, blocking accidental commits under system defaults | Claude | New: "Identity" |
gc.reflogExpire |
180.days |
Extend reflog retention for forensic readiness (default 90 days) | Claude | New: "Forensic Readiness" |
gc.reflogExpireUnreachable |
90.days |
Extend unreachable reflog retention (default 30 days) | Claude | New: "Forensic Readiness" |
transfer.bundleURI |
false |
Disable bundle URI fetching — reduces attack surface | Claude | Existing: "Object Integrity" |
protocol.version |
2 |
Wire protocol v2 — better performance, reduced attack surface from reference advertisements | Gemini | Existing: "Protocol Restrictions" |
init.defaultBranch |
main |
Modern default branch name | Claude | New: "Defaults" |
core.symlinks |
false |
Prevent symlink-based attacks (relevant to CVE-2024-32002). Interactive-only: prompt with default=yes, but skip in -y mode (may break symlink-dependent workflows like Node.js monorepos) |
Claude | Existing: "Filesystem Protection" |
fetch.prune |
true |
Auto-prune stale remote-tracking refs on fetch | Claude | Existing: "Object Integrity" |
Audit & Apply Behavior
Seven of eight follow the existing audit_git_setting / apply_git_setting pattern. core.symlinks is the exception:
- Audit: reports current value like all other settings.
- Interactive mode: prompts with default=yes ("Disable symlinks to prevent symlink-based attacks (CVE-2024-32002)? Note: this may break projects that use symlinks, e.g. Node.js monorepos. [Y/n]").
-ymode: skipscore.symlinksentirely (does not auto-apply). This is because disabling symlinks can silently break real workflows, and-ymode should not cause unexpected breakage.
Acceptance Criteria
- All 8 settings appear in audit output under their respective sections.
- All 8 settings are applied (with prompt or auto) in apply mode.
- Existing tests updated to cover new settings.
Feature 6: safe.directory Wildcard Detection
Background
The Claude report explicitly warns: "Never set safe.directory = *." This wildcard completely disables the ownership safety check introduced in Git 2.35.2 (CVE-2022-24765), allowing any user on a shared system to exploit a planted .git directory.
Audit Behavior
- Run
git config --global --get-all safe.directoryand check if any value is*. [WARN]if*is found: "safe.directory = * disables ownership checks (CVE-2022-24765). Remove this setting."- No output if
*is not found (this is not a setting we apply — absence of*is the correct state).
Apply Behavior
- If
*is detected, prompt the user: "Remove dangerous safe.directory = * setting?" - If accepted, run
git config --global --unset safe.directory '*'(note: must handle the case where multiple values exist — use--unset-allif needed, but only for the*value).
Section Placement
Added to existing "Repository Safety" section.
Acceptance Criteria
- Wildcard detected and warned about in audit mode.
- Apply mode offers to remove it.
- Non-wildcard
safe.directoryentries are not affected.
Audit Section Order (v0.2.0)
Updated ordering with new sections integrated:
- Identity (
user.useConfigOnly) - Object Integrity (existing +
transfer.bundleURI,fetch.prune) - Protocol Restrictions (existing +
protocol.version) - Filesystem Protection (existing +
core.symlinks) - Hook Control (existing)
- Pre-commit Hook (new — gitleaks)
- Repository Safety (existing +
safe.directorywildcard detection) - Pull/Merge Hardening (existing)
- Transport Security (existing)
- Credential Storage (existing)
- Credential Hygiene (new — plaintext file detection)
- Global Gitignore (new)
- Defaults (new —
init.defaultBranch) - Forensic Readiness (new — reflog retention)
- Visibility (existing)
- Signing Configuration (existing)
- SSH Configuration (existing)
- SSH Key Hygiene (new)
Non-Goals
- Package manager integration (no
brew installorapt install). - Modifying or deleting user files (credential files, SSH keys).
- Repository-level hardening (branch protection, CODEOWNERS — these remain in admin recommendations).
- CI/CD pipeline configuration.
- GPG signing support (the script remains SSH-signing focused).
Compatibility
Same as v0.1.0: Bash 3.2+, macOS and Linux. No new dependencies. Gitleaks is optional — the hook is safe without it.
Testing
- Extend existing BATS test suite to cover all new audit checks and apply actions.
- Add container test cases for gitleaks hook installation (with and without gitleaks present).
- Test
safe.directory = *detection and removal. - Test credential file detection with mock files.
- Test SSH key hygiene with various key types.