{"slug": "gitleaks-tutorial-12-steps-60-min-2026", "title": "Gitleaks Tutorial: 12 Steps, 60 Min [2026]", "summary": "Gitleaks, an MIT-licensed Go tool for detecting hard-coded passwords, API keys and tokens in Git repositories, remains actively maintained as of October 28, 2026, and a tutorial outlines a three-layer defense — a full commit-history scan, a pre-commit hook and a GitHub Actions gate — that can be set up in about 60 minutes. The tutorial notes GitHub processed 7.4 billion commits in September, and that a leaked key stays reachable in history after a follow-up commit deletes the file, so a working-tree scan misses it while a history scan finds it. Gitleaks deprecated its `detect` and `protect` commands in v8.19.0 in favor of `git` and `dir`, and the tool's pattern matching cannot determine whether a found key is still live, unlike TruffleHog's active-credential checks.", "body_md": "Gitleaks is an MIT-licensed Go tool that finds hard-coded passwords, API keys and tokens in Git repositories, and as of October 28, 2026 it remains an actively maintained project ([gitleaks.io](https://gitleaks.io/)). You can stack three defenses with it in about an hour: a scan of your full commit history, a pre-commit hook that stops a secret before it enters history, and a GitHub Actions gate that fails any pull request carrying one.\n\nVolume is the problem. GitHub processed 7.4 billion commits in September, according to [The New Stack](https://thenewstack.io/github-7-4b-commits-september/), and every one of them is a chance for a token to slip through. Worse, a leaked key stays reachable in history after a follow-up commit deletes the file. A scan of the working tree misses it. A scan of history finds it.\n\nThe pattern also fits agent-driven pipelines. If you already run a headless coding agent through [Cursor CLI in GitHub Actions](https://shattered.io/cursor-cli-github-actions-headless-agent-2026/), the same runner hygiene applies: a secret gate on every pull request means an agent mistake cannot ship a credential. The finished project holds a tuned `.gitleaks.toml`, a pre-commit config, a CI workflow, a baseline strategy for legacy code and a self-test script that proves the whole stack still works after every change.\n\nDon't miss new tech stories on Google\n\nAdd Shattered once in the Google app and our stories show up in your news suggestions.\n\n[Add now](https://www.google.com/preferences/source?hl=en&q=https%3A%2F%2Fshattered.io%2F)\n\n## What Gitleaks Catches and Where It Fits in 2026\n\nGitleaks matches text against a library of rules. Each rule pairs a regular expression with optional keywords, an entropy floor and allowlists. The keyword acts as a cheap prefilter, so the engine only runs the expensive regex on lines that contain a hint such as `ghp_` or `AKIA`. The entropy floor then discards low-randomness strings like `changeme123`, which look like secrets but are not. That combination keeps the tool fast on large histories and keeps noise tolerable.\n\nThe open-source project covers the common cloud, SaaS and source-host token formats out of the box. You can extend the default rule set with your own patterns, which matters because the secrets most likely to hurt you are internal ones that no public rule library knows about. An internal service token, a legacy signing key or a vendor credential with a custom prefix all need a custom rule.\n\n### Three scan modes\n\nGitleaks offers three entry points. `gitleaks git` walks commit history (or only staged changes when you add `--staged`). `gitleaks dir` scans files on disk and ignores Git entirely, which suits build artifacts and unpacked archives. `gitleaks stdin` reads piped text. Older tutorials use `detect` and `protect`. The project deprecated both in v8.19.0, and while they still run, new work should use `git` and `dir`.\n\n### Gitleaks, TruffleHog and GitHub native scanning\n\nGitleaks is not the only option, and it does not replace GitHub features. The table below compares the approaches by design, not by benchmark, because rule coverage shifts with every release and no public head-to-head number is stable enough to quote.\n\n| Approach | Where it runs | Main strength | Main limit | \n|---|---|---|---|\n| Gitleaks | Laptop, CI runner, any Git host | Fast, single binary, TOML rules you own, works offline | Pattern matching only, so it cannot tell you whether a key is still live | \n| TruffleHog | Laptop, CI runner, many data sources | Commonly used when checking whether a found credential is still active matters | Heavier setup, more network calls, different rule format | \n| GitHub secret scanning | Inside GitHub only | Hosted, no pipeline to maintain, GitHub keeps shipping fixes to it (October 2026 updates included) | Tied to GitHub, and advanced security features sit behind a paid license | \n| Pre-commit hook plus Gitleaks | Developer machine | Stops a secret before it exists in history | Developers can skip it with `--no-verify` | \n\nThe practical answer is to layer them. Run Gitleaks locally and in CI because it works on every host and you control the rules. Keep GitHub native scanning on as a second net if your plan includes it. Consider a verification-focused tool when you need to triage hundreds of historical findings and want to know which ones still open a door.\n\n## Prerequisites and Versions\n\nEverything below runs on Linux or macOS. Windows users should work inside WSL, because the hook scripts are Bash. Keep the versions in the table in mind when you copy commands, since flag names and config syntax have moved across Gitleaks releases.\n\n| Tool | Version | Why you need it | \n|---|---|---|\n| Git | A recent release recommended | Hooks, `--staged` scans and history walking. Git 2.54 adds config-based hooks, which pre-commit 4.6.0 and later handle better | \n| Gitleaks | Latest stable release (issue reports in October 2026 reference v8.30.1) | The scanner. Config syntax below needs v8.25.0 or newer | \n| pre-commit | v4.7.0 (newest release) | Manages the hook. It fixed `language: node` hooks for`git commit -a` and submodules after a v4.6.1 regression | \n| Python | A currently supported Python 3 release | Runs pre-commit and the report summarizer | \n| Bash and OpenSSL | Any current version | The self-test script generates random fake tokens | \n| GitHub repository | Actions enabled | Hosts the CI gate. The pull request gate works on GitHub Free, Pro and Team plans | \n| jq (optional) | 1.6 or newer | Quick JSON inspection of reports | \n| Docker (optional) | Any current release | Alternative way to run Gitleaks without installing it | \n\npre-commit itself costs nothing, and so does Gitleaks. Budget about 60 minutes the first time. The longest single wait is the first `pre-commit` run, because the hook compiles Gitleaks from source and pre-commit bootstraps a Go toolchain if it cannot find one.\n\nOne more prerequisite is mental. Treat every token you plant in this exercise as radioactive. Generate fakes at runtime, never paste a real credential into a test, and never push the demo repository to a public remote. Secret scanners on the receiving end, including GitHub push protection, will flag test tokens that look real.\n\n## Steps 1 to 3: Install Gitleaks and Run the First History Scan\n\n### Step 1: Install Gitleaks\n\nPick one install path. Homebrew is the fastest on macOS and Linux. Docker avoids a local install. Building from source suits locked-down machines, and the project build has moved to a recent Go toolchain (Go 1.24 at last check), so use a current Go release. For release binaries, take the newest tag from the project releases page rather than a version quoted in an old blog post.\n\n```\n# Option A: Homebrew\nbrew install gitleaks\n\n# Option B: Docker (no local install)\ndocker pull zricethezav/gitleaks:latest\n\n# Option C: build from source\ngit clone https://github.com/gitleaks/gitleaks.git\ncd gitleaks\nmake build\nsudo install -m 0755 gitleaks /usr/local/bin/gitleaks\n\n# Verify\ngitleaks version\n```\n\nWatch out for look-alike forks. Search results in October 2026 surface a third-party fork that publishes pre-release tags mimicking the upstream numbering, such as `8.30.1-v.63`, which its release page marks as a pre-release. Those are not upstream releases. Install from the official `gitleaks/gitleaks` repository only.\n\n### Step 2: Create a demo repository with a planted leak\n\nThe demo repo needs a secret that sits in history but not in the current files. That shape is the most common real-world failure: a developer commits a token, notices, deletes it in the next commit, and believes the problem is gone. The script below generates a fake GitHub-style token at runtime so no static string ever lands in your shell history or in this repo.\n\n```\nmkdir secret-guard-demo && cd secret-guard-demo\ngit init -b main\ngit config user.name \"Demo User\"\ngit config user.email \"[email protected]\"\n\nmkdir -p config src tests/fixtures reports scripts\n\nFAKE_TOKEN=\"ghp_$(openssl rand -hex 18)\"\ncat > config/settings.py <<EOF\nGITHUB_TOKEN = \"${FAKE_TOKEN}\"\nDEBUG = False\nEOF\ngit add . && git commit -m \"Add settings\"\n\n# The \"fix\": remove the token in a follow-up commit\ncat > config/settings.py <<EOF\nimport os\nGITHUB_TOKEN = os.environ[\"GITHUB_TOKEN\"]\nDEBUG = False\nEOF\ngit add . && git commit -m \"Read token from environment\"\n```\n\nTwo commits now exist. The second one looks clean, and a reviewer reading only the final diff would approve it. The first still holds the token, and anyone who clones the repository can run `git show` on it.\n\n### Step 3: Scan history, then scan the working tree\n\nRun both modes against the same repository and compare. The history scan should flag the first commit. The directory scan should come back clean, because the current files no longer contain the token.\n\n```\n# Scan every commit (exits with code 1 when leaks are found)\ngitleaks git . --no-banner --redact --verbose\necho \"exit code: $?\"\n\n# Scan only the files on disk\ngitleaks dir . --no-banner --redact\necho \"exit code: $?\"\n```\n\nExpect output shaped like the block below. Your commit hash, timestamps and fingerprint will differ, and depending on the rule set you may see a second finding from a generic API key rule. The values shown are illustrative placeholders, not captured output.\n\n```\nFinding:     GITHUB_TOKEN = \"REDACTED\"\nSecret:      REDACTED\nRuleID:      github-pat\nEntropy:     3.7\nFile:        config/settings.py\nLine:        1\nCommit:      <commit-sha>\nAuthor:      Demo User\nEmail:       [email protected]\nDate:        2026-10-28T14:02:11Z\nFingerprint: <commit-sha>:config/settings.py:github-pat:1\n\nINF 2 commits scanned.\nINF scanned ~120 bytes in 18ms\nWRN leaks found: 1\nexit code: 1\n```\n\nThe directory scan prints no findings and exits with code 0. That gap is the whole argument for scanning history. If your CI only runs `gitleaks dir`, it would pass this repository while a live credential sits two commits back.\n\nNote the `--redact` flag. It replaces the secret value in logs and standard output, and the default strength is 100 percent. You can pass a percentage from 0 to 100 to show part of the value, for example `--redact=20`. Make redaction your default everywhere a scan result might land in a CI log, a ticket or a chat message.\n\n## Steps 4 to 5: Read Reports and Lock Down Their Permissions\n\n### Step 4: Export a JSON report and summarize it\n\nConsole output is fine for humans. Pipelines need structured data. Gitleaks writes JSON, CSV, JUnit, SARIF or template output through `--report-format` and `--report-path`. JSON is the easiest to post-process, and each finding carries fields such as `RuleID`, `File`, `StartLine`, `Commit`, `Entropy` and `Fingerprint`.\n\n```\numask 077\ngitleaks git . --no-banner --redact \\\n  --report-format json --report-path reports/gitleaks.json\n\njq '.[] | {RuleID, File, StartLine, Commit, Fingerprint}' reports/gitleaks.json\n```\n\nSave the Python helper below as `scripts/summarize_report.py`. It groups findings by rule and file, prints a Markdown table that renders well in a GitHub step summary, and exits non-zero when anything is present. It never prints a secret value, even if someone forgot `--redact`, because it reads only metadata fields.\n\n``` python\n#!/usr/bin/env python3\nimport collections\nimport json\nimport pathlib\nimport sys\n\ndef main(path: str) -> int:\n    report = pathlib.Path(path)\n    if not report.exists():\n        print(\"No report found, nothing to summarize.\")\n        return 0\n\n    findings = json.loads(report.read_text() or \"[]\")\n    if not findings:\n        print(\"No secrets found.\")\n        return 0\n\n    by_rule = collections.Counter(f[\"RuleID\"] for f in findings)\n    print(f\"### Gitleaks found {len(findings)} potential secret(s)\\n\")\n    print(\"| Rule | Count |\")\n    print(\"|---|---|\")\n    for rule, count in by_rule.most_common():\n        print(f\"| `{rule}` | {count} |\")\n\n    print(\"\\n| File | Line | Commit | Rule |\")\n    print(\"|---|---|---|---|\")\n    for f in findings:\n        commit = (f.get(\"Commit\") or \"working-tree\")[:10]\n        print(f\"| `{f['File']}` | {f['StartLine']} | `{commit}` | `{f['RuleID']}` |\")\n    return 1\n\nif __name__ == \"__main__\":\n    sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else \"reports/gitleaks.json\"))\n```\n\n### Step 5: Keep reports out of Git and out of reach\n\nA report is a map to your secrets. Even a redacted one lists file paths, commit hashes and rule names that tell an attacker where to look. An issue opened on the Gitleaks tracker in October 2026 reports that, in versions through v8.30.1, report files containing discovered secrets are created with permissive default permissions. Treat that as a reason to set `umask 077` before every scan, as shown above, and to add the reports directory to `.gitignore`.\n\n```\ncat > .gitignore <<'EOF'\nreports/\n*.sarif\ngitleaks-report*.json\n.env\n.env.*\nEOF\n\ngit add .gitignore && git commit -m \"Ignore scan reports and env files\"\n```\n\nOne more habit: if you must generate an unredacted report to triage a real incident, write it to a temporary directory, read it, then shred it. Do not upload it as a CI artifact, and do not paste it into a ticket.\n\n## Steps 6 to 8: Tune Gitleaks With a Config File\n\nThe default rules catch public token formats. They cannot know your internal ones, and they will occasionally flag harmless strings in test data. A `.gitleaks.toml` file fixes both problems. Gitleaks resolves its config in a fixed order: the `--config` (or `-c`) flag first, then the `GITLEAKS_CONFIG` environment variable, then `GITLEAKS_CONFIG_TOML`, and finally a `.gitleaks.toml` in the target path. Committing the file at the repository root is the simplest setup, since every hook and CI job picks it up with no extra flags.\n\n### Step 6: Extend the defaults and add a custom rule\n\nStart with `[extend]` and `useDefault = true`. Without it, your config replaces the built-in rules entirely, and a repo that suddenly stops catching GitHub tokens is a nasty surprise. Then add one rule for a fictional internal token format, `acme_live_` or `acme_test_` followed by 32 alphanumeric characters.\n\n```\ntitle = \"secret-guard-demo\"\n\n[extend]\nuseDefault = true\n\n[[rules]]\nid = \"acme-internal-token\"\ndescription = \"ACME internal service token\"\nregex = '''acme_(live|test)_[a-zA-Z0-9]{32}'''\nkeywords = [\"acme_\"]\nentropy = 3.0\ntags = [\"internal\", \"acme\"]\n\n# Rule-level allowlist: test-tier tokens carry no production access\n[[rules.allowlists]]\ndescription = \"ACME test tokens are harmless\"\nregexes = ['''acme_test_[a-zA-Z0-9]{32}''']\n```\n\nThe `keywords` array makes the rule cheap, because Gitleaks skips the regex on any line that lacks `acme_`. The `entropy` floor of 3.0 drops placeholders such as a run of repeated zeros. Keep the regex as tight as the real format allows. A loose pattern like `[a-z0-9]{32}` would flag every MD5 hash in your lockfiles.\n\nTest the rule before you trust it. Generate one fake live token and one fake test token, then scan the directory with only your rule enabled.\n\n```\nmkdir -p /tmp/rule-check\nprintf 'LIVE=acme_live_%s\\n' \"$(openssl rand -hex 16)\" > /tmp/rule-check/live.env\nprintf 'TEST=acme_test_%s\\n' \"$(openssl rand -hex 16)\" > /tmp/rule-check/test.env\n\ngitleaks dir /tmp/rule-check --config .gitleaks.toml \\\n  --enable-rule acme-internal-token --no-banner --redact --verbose\nrm -rf /tmp/rule-check\n```\n\nYou should see exactly one finding, for `live.env`. The test token passes because of the rule-level allowlist. If both files flag, check that you used `[[rules.allowlists]]` (double brackets) and that the block sits directly under the rule it belongs to.\n\n### Step 7: Add narrow allowlists for fixtures and lockfiles\n\nGlobal allowlists suppress findings across every rule. Use them sparingly and scope them tightly. A good global allowlist names one directory or one file pattern, and it explains itself in the `description` field so the next reviewer knows why it exists.\n\n```\n[[allowlists]]\ndescription = \"Synthetic fixtures used by unit tests\"\npaths = ['''(^|/)tests/fixtures/''']\n\n[[allowlists]]\ndescription = \"Lockfiles hold integrity hashes, not credentials\"\npaths = [\n  '''(^|/)package-lock\\.json$''',\n  '''(^|/)poetry\\.lock$''',\n  '''(^|/)go\\.sum$''',\n]\n```\n\nSince v8.25.0, global allowlists support a `targetRules` field, which restricts an allowlist to specific rule IDs. That is the safest option for a noisy rule. Instead of silencing the whole `tests` directory for every rule, you silence only `generic-api-key` there. A real GitHub token pasted into a test file still trips the `github-pat` rule.\n\n```\n[[allowlists]]\ndescription = \"Generic key rule is noisy in snapshot tests\"\ntargetRules = [\"generic-api-key\"]\npaths = ['''(^|/)tests/snapshots/''']\n```\n\nYou can also combine conditions. Setting `condition = \"AND\"` inside an allowlist requires every listed matcher (a path and a regex, for example) to match before the finding is suppressed. The default is OR, which is why a careless allowlist with a broad path and a broad regex hides far more than intended.\n\n### Step 8: Handle one-off exceptions without touching the config\n\nNot every exception deserves a config change. Gitleaks gives you two lightweight escape hatches. An inline comment containing `gitleaks:allow` on the offending line suppresses that single finding. A `.gitleaksignore` file at the repo root lists fingerprints, one per line, with `#` comments allowed. The fingerprint comes straight from the scan output (commit, file, rule and line for history scans).\n\n```\n# .gitleaksignore\n# Revoked in 2026-09 after rotation (ticket SEC-142). Safe to ignore.\n<commit-sha>:config/settings.py:github-pat:1\n# In source: suppress one line, with a reason a reviewer can read\nEXAMPLE_KEY = \"acme_test_00000000000000000000000000000000\"  # gitleaks:allow\n```\n\nUse fingerprints for secrets you have already revoked, since the history cannot change without a rewrite. Use inline allows for documented examples. Require a ticket number or a date in the comment, and review the ignore file in every security audit. An ignore list with no owner grows quietly until it hides a live key.\n\n### Know the syntax changes that break old configs\n\nConfig syntax has changed several times in the 8.x line, so a snippet copied from an older post may fail to parse or may silently do nothing. The table lists the breakpoints that matter most.\n\n| Gitleaks version | Change | What breaks | \n|---|---|---|\n| v8.8.0 | Stopwords introduced for allowlists | Configs using `stopwords` need this version or newer | \n| v8.19.0 | `detect` and`protect` deprecated in favor of`git` and`dir` | Old commands still run, but docs, hooks and CI snippets should migrate | \n| v8.21.0 | `[rules.allowlist]` replaced by`[[rules.allowlists]]` | Single-bracket rule allowlists from older posts stop applying as intended | \n| v8.25.0 | `[allowlist]` replaced by`[[allowlists]]` , new`targetRules` field | Global allowlists written the old way need rewriting | \n\nWhen an upgrade changes behavior, diff your findings before and after. Run the old and new versions against the same repo with JSON reports, then compare fingerprints. Any finding that appears or disappears points at a syntax or rule change you need to understand.\n\n## Steps 9 and 10: Block Secrets Before They Enter History\n\nEvery secret you stop at commit time is a secret you never have to rotate, rewrite out of history or explain to an auditor. A local hook is the cheapest control in this stack. Its weakness is that the developer controls it, so treat the hook as a speed bump with good manners and let CI act as the wall.\n\n### Step 9: Wire Gitleaks into the pre-commit framework\n\nThe [pre-commit](https://pre-commit.com/) framework manages multi-language Git hooks from one YAML file, and the project is free and open source. The newest release at the time of writing is v4.7.0, published October 13, 2026. Pin your tooling and test it, rather than assuming behavior carried over from an older release. Install it with pip or pipx, then create the config.\n\n```\npipx install pre-commit\npre-commit --version\n\ncat > .pre-commit-config.yaml <<'EOF'\nrepos:\n  - repo: https://github.com/gitleaks/gitleaks\n    rev: v8.30.1\n    hooks:\n      - id: gitleaks\nEOF\n\n# Move the pinned rev to the newest upstream tag whenever you like\npre-commit autoupdate --repo https://github.com/gitleaks/gitleaks\n\npre-commit install\n```\n\nThe `rev` above is the tag that Gitleaks issue reports reference in October 2026. Always run `autoupdate` once and commit whatever tag it lands on. Add `--freeze` if you want pre-commit to record the full commit SHA with the tag in a comment, which is the safer pin against a moved tag.\n\nThe stock hook runs Gitleaks against staged changes only. That means `pre-commit run --all-files` does not walk your history, so it is not a substitute for the `gitleaks git` scan from Step 3. Use the hook for prevention and the CLI for audits.\n\nNow prove the hook works. Stage a file holding a fresh fake token and try to commit.\n\n```\nprintf 'API=acme_live_%s\\n' \"$(openssl rand -hex 16)\" > src/client.env\ngit add src/client.env\ngit commit -m \"Add client config\"\ngitleaks..................................................................Failed\n- hook id: gitleaks\n- exit code: 1\n\nFinding:     API=REDACTED\nSecret:      REDACTED\nRuleID:      acme-internal-token\nEntropy:     3.8\nFile:        src/client.env\nLine:        1\n\nWRN leaks found: 1\n```\n\nThe commit is rejected and nothing reaches history. Unstage the file with `git restore --staged src/client.env` and delete it. Expect the very first hook run to take noticeably longer than later ones, because pre-commit builds the hook environment, and for a Go hook it may bootstrap a Go toolchain. Subsequent runs reuse the cache and finish quickly on a normal change set. The output above shows the typical shape, not captured text, so your wording and entropy value will differ.\n\n### Step 10: Add a plain Git hook as a fallback\n\nSome teams cannot use pre-commit. A repository may already set `core.hooksPath`, a corporate image may block pipx, or a developer may want zero Python on the machine. A native hook script covers those cases. Keep it in the repository under `.githooks/` so it is versioned and reviewable, then point Git at the directory.\n\n``` bash\nmkdir -p .githooks\ncat > .githooks/pre-commit <<'EOF'\n#!/usr/bin/env bash\nset -euo pipefail\n\nif ! command -v gitleaks >/dev/null 2>&1; then\n  echo \"gitleaks is not installed. Install it, then retry the commit.\" >&2\n  echo \"macOS/Linux: brew install gitleaks\" >&2\n  exit 1\nfi\n\nexec gitleaks git --pre-commit --staged --redact --verbose --no-banner\nEOF\nchmod +x .githooks/pre-commit\n\ngit config core.hooksPath .githooks\n```\n\nThe script fails closed. If Gitleaks is missing, the commit stops with install instructions. Some teams prefer failing open so that a missing tool never blocks urgent work. That is a policy choice, but make it deliberately, because a hook that silently skips on half the laptops in the company protects nothing.\n\nDo not run both hooks at once. pre-commit refuses to install while `core.hooksPath` is set, which is a deliberate safety check. Pick one mechanism per repository. Teams on Git 2.54 or newer can look at config-based hooks, which pre-commit has supported more completely since v4.6.0.\n\nRemember the bypass. `git commit --no-verify` skips every client-side hook. Developers use it for good reasons, such as a broken hook on a Friday evening, and for bad ones. Neither case should be able to push a secret into the main branch, and that is the job of the CI gate in the next section.\n\n## Steps 11 and 12: Gate Pull Requests in GitHub Actions and Baseline Legacy Code\n\n### Step 11: Create the CI workflow\n\nCI is the control a developer cannot skip with a flag. The workflow below checks out the full history, installs a pinned Gitleaks release with a checksum check, scans only the commit range the event introduced, and writes a summary to the job page. Scanning only the new range keeps pull request jobs fast on large repositories, while a scheduled full-history scan (see the advanced tips) catches anything the range logic misses.\n\nThe workflow installs the release binary directly instead of using a third-party action or the Docker image. That choice gives you one explicit version to bump, a verified download and no dependency on a moving `latest` tag. Set `GITLEAKS_VERSION` to the newest upstream release before you commit the file. The value below is the tag that issue reports reference in October 2026.\n\n```\n# .github/workflows/secret-scan.yml\nname: secret-scan\n\non:\n  pull_request:\n  push:\n    branches: [main]\n\npermissions:\n  contents: read\n\nconcurrency:\n  group: secret-scan-${{ github.ref }}\n  cancel-in-progress: true\n\nenv:\n  GITLEAKS_VERSION: \"8.30.1\"\n\njobs:\n  gitleaks:\n    runs-on: ubuntu-latest\n    timeout-minutes: 10\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n          persist-credentials: false\n\n      - name: Install Gitleaks\n        run: |\n          set -euo pipefail\n          base=\"https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}\"\n          tarball=\"gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz\"\n          curl -sSfL -o \"${tarball}\" \"${base}/${tarball}\"\n          curl -sSfL -o checksums.txt \"${base}/gitleaks_${GITLEAKS_VERSION}_checksums.txt\"\n          grep \" ${tarball}$\" checksums.txt | sha256sum -c -\n          tar -xzf \"${tarball}\" gitleaks\n          sudo install -m 0755 gitleaks /usr/local/bin/gitleaks\n          gitleaks version\n\n      - name: Scan new commits\n        env:\n          EVENT_NAME: ${{ github.event_name }}\n          BASE_SHA: ${{ github.event.pull_request.base.sha }}\n          HEAD_SHA: ${{ github.event.pull_request.head.sha }}\n          BEFORE_SHA: ${{ github.event.before }}\n          PUSH_SHA: ${{ github.sha }}\n        run: |\n          set -euo pipefail\n          umask 077\n          mkdir -p reports\n          args=(--config .gitleaks.toml --redact --no-banner\n                --report-format json --report-path reports/gitleaks.json)\n          if [ -f .gitleaks-baseline.json ]; then\n            args+=(--baseline-path .gitleaks-baseline.json)\n          fi\n          zeros=\"0000000000000000000000000000000000000000\"\n          if [ \"${EVENT_NAME}\" = \"pull_request\" ]; then\n            args+=(\"--log-opts=${BASE_SHA}..${HEAD_SHA}\")\n          elif [ \"${BEFORE_SHA}\" != \"${zeros}\" ]; then\n            args+=(\"--log-opts=${BEFORE_SHA}..${PUSH_SHA}\")\n          fi\n          set +e\n          gitleaks git . \"${args[@]}\"\n          status=$?\n          set -e\n          python3 scripts/summarize_report.py reports/gitleaks.json >> \"${GITHUB_STEP_SUMMARY}\" || true\n          exit \"${status}\"\n\n      - name: Upload redacted report\n        if: failure()\n        uses: actions/upload-artifact@v4\n        with:\n          name: gitleaks-report\n          path: reports/gitleaks.json\n          retention-days: 7\n```\n\nSeveral details earn their place. The `permissions: contents: read` block gives the job the least privilege it needs. Passing event values through `env` rather than interpolating them into the script body avoids shell injection from attacker-controlled fields. The zero-SHA check handles the first push to a new branch, where there is no previous commit to diff against. And `fetch-depth: 0` is non-negotiable, because the default shallow clone fetches one commit and the scan would inspect almost nothing.\n\nOpen a pull request that adds a fake `acme_live_` token and watch the job fail. The step summary lists the rule, file, line and short commit hash, and the job exits with code 1. Then make the check required: in the repository settings, add a branch protection rule or ruleset that requires the `gitleaks` job to pass before merge. GitHub added bulk conversion of branch protection rules into rulesets on October 20, 2026, so teams with many repositories can migrate the requirement in one pass.\n\n### What changed on GitHub Actions in late October 2026\n\nThree recent platform changes touch this workflow. First, on October 28, 2026 GitHub began scheduling eligible Linux jobs on its standard 2-core and 4-core hosted runners across both Microsoft Azure and Amazon Web Services. Workflow syntax and runner labels stay the same, but organizations that restrict egress by IP must allow all published Actions IP ranges. If your firewall blocks the download in the install step, that is the likely cause. GitHub lists the change in its [October 2026 changelog](https://github.blog/changelog/month/10-2026/page/2/).\n\nSecond, GitHub is retiring the `macos-14` runner images on November 2, 2026. This workflow uses `ubuntu-latest`, so it is unaffected. If you copy the job into a matrix that includes macOS, move to `macos-15` or `macos-latest` first. Third, workflow dependency lockfiles entered public preview on October 14, 2026 for the Free, Team and Enterprise Cloud plans. They target exactly the supply chain risk this workflow shares with every other one: actions referenced by a mutable tag.\n\n### Step 12: Baseline the findings you cannot fix today\n\nPoint Gitleaks at a ten-year-old repository and the first scan may return hundreds of findings, most of them long since revoked. Failing every pull request on that backlog teaches developers to ignore the check. A baseline solves it. Gitleaks accepts an earlier report through `--baseline-path` and suppresses every finding already in it, so the gate fires only on new leaks.\n\n```\n# Create the baseline once, redacted, on the default branch\numask 077\ngitleaks git . --no-banner --redact \\\n  --report-format json --report-path .gitleaks-baseline.json\n\n# Review it, then commit it\npython3 scripts/summarize_report.py .gitleaks-baseline.json\ngit add .gitleaks-baseline.json\ngit commit -m \"Add Gitleaks baseline for pre-existing findings\"\n\n# Later scans ignore baselined findings\ngitleaks git . --no-banner --redact --baseline-path .gitleaks-baseline.json\n```\n\nDo two things before you commit that file. First, always generate it with `--redact`. An unredacted baseline stores every historical secret in plain text inside the repository, which turns your safety net into the leak. Second, treat the baseline as a debt ledger, not a pardon. Revoke each credential it lists, and shrink the file over time. A baseline that never gets smaller means nobody owns the cleanup.\n\nBaselines and `.gitleaksignore` overlap, so pick a division of labor. Use the baseline for bulk legacy findings and the ignore file for individual, explained exceptions. Both match on fingerprints, so a rewritten history invalidates both and forces a regeneration.\n\n## The Complete Working Project: Self-Test, Makefile and File Tree\n\nA secret scanner that nobody tests rots. Rules get edited, allowlists widen, and one day a real token sails through because a regex lost a character. The self-test below plants fake tokens in throwaway repositories, runs Gitleaks with the project config, and fails loudly if detection breaks or if an allowlist suppresses too much. Run it locally before every change to `.gitleaks.toml` and in CI on a schedule.\n\n``` bash\n#!/usr/bin/env bash\n# scripts/selftest.sh\nset -euo pipefail\n\nROOT=\"$(cd \"$(dirname \"$0\")/..\" && pwd)\"\nWORK=\"$(mktemp -d)\"\ntrap 'rm -rf \"${WORK}\"' EXIT\n\nmake_repo() {\n  local dir=\"$1\"\n  mkdir -p \"${dir}\"\n  git -C \"${dir}\" init -q -b main\n  git -C \"${dir}\" config user.name \"selftest\"\n  git -C \"${dir}\" config user.email \"[email protected]\"\n}\n\nscan() {\n  gitleaks git \"$1\" --config \"${ROOT}/.gitleaks.toml\" \\\n    --redact --no-banner --log-level error >/dev/null 2>&1\n}\n\n# Case 1: a live internal token must be caught in history\nmake_repo \"${WORK}/live\"\nprintf 'TOKEN=acme_live_%s\\n' \"$(openssl rand -hex 16)\" > \"${WORK}/live/app.env\"\ngit -C \"${WORK}/live\" add . && git -C \"${WORK}/live\" commit -q -m \"plant live token\"\ngit -C \"${WORK}/live\" rm -q app.env && git -C \"${WORK}/live\" commit -q -m \"remove it\"\nif scan \"${WORK}/live\"; then\n  echo \"FAIL: live token buried in history was not detected\" >&2\n  exit 1\nfi\necho \"PASS: live token detected in history\"\n\n# Case 2: a GitHub-style token must be caught by the default rules\nmake_repo \"${WORK}/pat\"\nprintf 'GH=ghp_%s\\n' \"$(openssl rand -hex 18)\" > \"${WORK}/pat/ci.env\"\ngit -C \"${WORK}/pat\" add . && git -C \"${WORK}/pat\" commit -q -m \"plant pat\"\nif scan \"${WORK}/pat\"; then\n  echo \"FAIL: default rules did not fire, check [extend] useDefault\" >&2\n  exit 1\nfi\necho \"PASS: default rules still active\"\n\n# Case 3: a test-tier token must be allowed by the rule-level allowlist\nmake_repo \"${WORK}/test\"\nprintf 'TOKEN=acme_test_%s\\n' \"$(openssl rand -hex 16)\" > \"${WORK}/test/app.env\"\ngit -C \"${WORK}/test\" add . && git -C \"${WORK}/test\" commit -q -m \"plant test token\"\nif ! scan \"${WORK}/test\"; then\n  echo \"FAIL: test-tier token should be allowlisted\" >&2\n  exit 1\nfi\necho \"PASS: test-tier token allowlisted\"\n\necho \"All self-tests passed.\"\n```\n\nThe three cases cover the three ways a config usually fails: a custom rule that stops matching, default rules that disappear, and an allowlist that is either too narrow or too wide. If you add a global allowlist later, add a fourth case that proves a real-looking token inside the allowlisted path is handled the way you intend.\n\nA Makefile ties the commands together so a new teammate runs two targets instead of reading this walkthrough.\n\n```\n# Makefile\n.PHONY: setup scan history selftest baseline\n\nsetup:\n\tpipx install pre-commit\n\tpre-commit install\n\nscan:\n\tumask 077 && mkdir -p reports && \\\n\tgitleaks dir . --no-banner --redact --config .gitleaks.toml\n\nhistory:\n\tumask 077 && mkdir -p reports && \\\n\tgitleaks git . --no-banner --redact --config .gitleaks.toml \\\n\t  --report-format json --report-path reports/gitleaks.json; \\\n\tpython3 scripts/summarize_report.py reports/gitleaks.json\n\nselftest:\n\tbash scripts/selftest.sh\n\nbaseline:\n\tumask 077 && gitleaks git . --no-banner --redact --config .gitleaks.toml \\\n\t  --report-format json --report-path .gitleaks-baseline.json\n```\n\nYour repository should now match this layout. Every file came from a step above.\n\n```\nsecret-guard-demo/\n|-- .githooks/\n|   `-- pre-commit              # Step 10 (fallback hook)\n|-- .github/\n|   `-- workflows/\n|       `-- secret-scan.yml     # Step 11\n|-- config/\n|   `-- settings.py             # Step 2 (history holds the planted token)\n|-- reports/                    # Step 4 (git-ignored)\n|-- scripts/\n|   |-- selftest.sh             # self-test above\n|   `-- summarize_report.py     # Step 4\n|-- tests/fixtures/             # Step 7 (allowlisted path)\n|-- .gitignore                  # Step 5\n|-- .gitleaks-baseline.json     # Step 12 (optional, redacted)\n|-- .gitleaks.toml              # Steps 6 and 7\n|-- .gitleaksignore             # Step 8\n|-- .pre-commit-config.yaml     # Step 9\n`-- Makefile\n```\n\nFinish with a full verification pass. Run the self-test, scan history, then try to commit a fresh fake token to prove the hook still blocks it.\n\n```\nmake selftest\nmake history\nprintf 'X=acme_live_%s\\n' \"$(openssl rand -hex 16)\" > leak-test.env\ngit add leak-test.env && git commit -m \"should be blocked\"\ngit restore --staged leak-test.env && rm leak-test.env\n```\n\nExpect the self-test to print three PASS lines followed by the closing message, the history scan to report the Step 2 token (unless you baselined it), and the commit to fail. If all three behave that way, the stack works end to end.\n\n## When a Secret Gets Through: A Five-Step Response\n\nScanners reduce risk and never remove it. Plan the response before you need it, because the first hour decides whether a leak becomes an incident. The ordering matters. Rotation comes first, cleanup comes last, and nothing about cleanup undoes exposure. For a broader view of detection and response practice, see the [security coverage](https://shattered.io/category/security/) on this site.\n\n1. **Revoke and rotate immediately.** Assume anyone who cloned, forked or cached the repository has the credential. Issue a new one and kill the old one at the provider, before you touch Git.\n2. **Check usage.** Pull the provider audit logs for the exposure window. Look for requests from unfamiliar IP addresses, new resources and permission changes. Record what you find even if it is nothing.\n3. **Remove it from history if policy requires.** Use`git filter-repo --replace-text` on a fresh mirror clone to scrub the string from every commit, then force-push and ask collaborators to re-clone. This step tidies the record. It does not make the secret safe again.\n4. **Record the fingerprint.** Once the credential is dead, add its fingerprint to`.gitleaksignore` with the ticket number, so the baseline stays quiet and the audit trail stays complete.\n5. **Add a regression test.** If the leaked format had no rule, write one, add a case to`selftest.sh` and ship it. Every incident should leave the scanner stronger.\n\n```\n# Scrub a leaked string from all history (run in a fresh mirror clone)\ngit clone --mirror [email protected]:org/repo.git repo-mirror.git\ncd repo-mirror.git\nprintf '%s==>REMOVED-SECRET\\n' \"THE-LEAKED-STRING\" > ../replacements.txt\ngit filter-repo --replace-text ../replacements.txt\ngit push --force --mirror\nshred -u ../replacements.txt 2>/dev/null || rm -f ../replacements.txt\n```\n\nPull requests, forks and any cached views on a Git host may keep the old commits reachable after a force push, and your host may need to purge them on request. This is one more reason to put rotation first.\n\n## Rolling Gitleaks Out to a Team Without Starting a Revolt\n\nThe technical setup takes an hour. The social setup takes a few weeks, and it decides whether the tool survives. Developers accept a gate that rarely cries wolf and explains itself. They resent one that blocks a release over a string in a test file. Plan the rollout in stages and give each stage an owner.\n\n### Week one: observe without blocking\n\nRun the CI job in report-only mode first. Let it post the step summary but exit with code 0, either by adding `continue-on-error: true` to the scan step or by ignoring the exit status in the script. Collect every finding from a full-history scan across your repositories. Sort them into three buckets: live credentials that need rotation today, revoked credentials that belong in the baseline, and false positives that need a rule or allowlist change. The first bucket is usually the smallest and always the most valuable.\n\n### Week two: fix the rules, then turn on the gate\n\nSpend this week on tuning. Every false positive you fix now saves a hundred annoyed pull requests later. Tighten regexes, add scoped allowlists with a reason, and generate the redacted baseline for each repository. Only then flip the job to blocking and mark it as a required check. Announce the change in the engineering channel with a short note on how to read a finding, how to request an exception and who to ask for help.\n\n### Week three and beyond: add the hook and measure\n\nRoll out the pre-commit hook once the CI gate is stable. A hook that fires before the team trusts the rules feels like surveillance, while a hook that mirrors a check they already know feels like a convenience. Track a few simple signals without inventing targets: how many findings the hook blocks per month, how many pull requests the CI gate fails, how long exceptions stay in the ignore file, and how many baselined findings remain. A falling baseline count is the best single sign that the program works.\n\nAssign ownership for two recurring chores. Someone reviews the ignore file and baseline every quarter and removes entries for credentials that no longer exist. Someone else owns version bumps for Gitleaks, pre-commit and the workflow, and reads the upstream release notes for syntax changes before merging them. Small, boring ownership beats a heroic cleanup every time, because secret scanning is a habit rather than a project.\n\nFinally, write down the exception policy. Decide who can approve an allowlist entry, whether an exception needs an expiry date and what evidence proves a credential is dead. A one-page policy removes most arguments, and it gives auditors something concrete to read when they ask how the organization handles leaked credentials.\n\n## Common Pitfalls That Break Secret Scanning\n\nMost failed rollouts trace back to the same handful of mistakes. Each of the eight below has burned a real team, and each takes minutes to avoid once you know it exists.\n\n1. **Using deprecated commands.** Tutorials from before v8.19.0 teach`gitleaks detect` and`gitleaks protect` . They still run, but the project deprecated them, and mixed old and new syntax across hooks and CI makes upgrades painful. Standardize on`git` and`dir` .\n2. **Scanning shallow clones.** GitHub Actions checks out a single commit by default. A history scan on that checkout inspects almost nothing and still reports success. Set`fetch-depth: 0` and confirm the commit count in the log line.\n3. **Trusting the working tree.** As Step 3 showed,`gitleaks dir` cannot see a token that a later commit removed. Always include at least one history scan in the pipeline.\n4. **Leaking through the report.** An unredacted JSON report, a debug log or an uploaded artifact can hold every secret you just found. Use`--redact` everywhere, set`umask 077` , and git-ignore the reports directory.\n5. **Allowlisting too much.** A pattern like`\\.md$` or a bare`.*` regex hides real leaks in documentation, where people love pasting credentials. Scope by directory, by rule through`targetRules` , and by a documented reason.\n6. **Believing a delete fixes a leak.** Removing the file, rewriting history or making the repository private does not recall a credential someone already copied. Rotate first.\n7. **Relying on the hook alone.** Hooks run on the developer machine, and`--no-verify` skips them. Without a required CI check, the hook is advice rather than policy.\n8. **Floating versions.** A`latest` Docker tag or an unpinned download changes behavior without a commit in your repo. Pin the version, verify the checksum and bump on purpose.\n\nA ninth mistake deserves a mention: ignoring the findings. A scanner that fails every build on noise gets disabled within a week. Spend the first day tuning rules and baselining, so the first red check a developer sees is a real one.\n\n## Troubleshooting: 11 Problems and Their Fixes\n\nWork through the table top to bottom when something misbehaves. Most symptoms map to a single cause.\n\n| Symptom | Likely cause | Fix | \n|---|---|---|\n| Config fails to parse after an upgrade, or allowlists seem ignored | Old `[allowlist]` or`[rules.allowlist]` syntax | Rewrite to `[[allowlists]]` and`[[rules.allowlists]]` , which v8.25.0 and v8.21.0 introduced | \n| Deprecation warnings mentioning `detect` or`protect` | Hooks or scripts written before v8.19.0 | Switch to `gitleaks git` and`gitleaks dir` | \n| CI passes but scans one commit | Shallow checkout | Add `fetch-depth: 0` to`actions/checkout` | \n| `fatal: detected dubious ownership` in a container | Repository owned by a different user than the one running Git | Run `git config --global --add safe.directory /path` before the scan | \n| First pre-commit run takes minutes or fails to build | pre-commit compiles the Go hook and may bootstrap Go, which needs network access | Allow the network on first run, then rely on the cache. Use the native hook from Step 10 on locked-down machines | \n| `Cowardly refusing to install hooks with core.hooksPath set` | Both pre-commit and a custom hooks path are active | Run `git config --unset core.hooksPath` or use only the native hook | \n| A custom hook fails before scanning when several files are staged | pre-commit passes filenames to a system-level hook that does not expect them | Prefer the stock `gitleaks` hook. A proposed upstream fix sets`pass_filenames: false` , so add that to any custom local hook | \n| Every legacy finding fails the build | No baseline | Create a redacted baseline (Step 12) and pass `--baseline-path` | \n| Job fails with `fatal: bad object` on a push event | A force-push removed the commit named by `github.event.before` | Catch the failed range lookup in the script and fall back to a full-history scan for that run | \n| Release download fails on runners behind a firewall | GitHub began spreading eligible Linux jobs across Azure and AWS on October 28, 2026 | Allow all published GitHub Actions IP ranges in your egress rules | \n| Scan flags MD5 hashes or lockfile checksums | Loose custom regex or the generic key rule | Tighten the regex, raise `entropy` , or add a path allowlist scoped with`targetRules` | \n\nIf none of these match, rerun with `--log-level debug` and read the first few lines. Gitleaks prints which config file it loaded, and a surprising path there explains many mysteries. Check whether an environment variable such as `GITLEAKS_CONFIG` is overriding your file.\n\n## Advanced Tips for Larger Teams\n\n### Scan encoded content and archives\n\nAttackers and careless developers both hide secrets in Base64 blobs. Recursive decoding is off by default, and you enable it with `--max-decode-depth` followed by a positive number. Raise it cautiously. Each level multiplies scan time and false positives, and a depth of two or three covers most real cases such as a Base64-wrapped JSON key file.\n\n### Add a scheduled full-history scan\n\nThe pull request job scans only new commits, so it cannot notice a rule you add next month that would have caught an old leak. A weekly scheduled workflow that runs a full history scan with your baseline closes that gap. Add `schedule` with a cron expression to the workflow triggers, drop the `--log-opts` argument for that event, and alert the security channel on failure.\n\n### Sweep every repository in an organization\n\n``` bash\n#!/usr/bin/env bash\n# scan-org.sh: mirror each repo, scan it, keep redacted reports\nset -euo pipefail\nORG=\"your-org\"\nmkdir -p org-scan && cd org-scan\numask 077\ngh repo list \"${ORG}\" --limit 1000 --json nameWithOwner --jq '.[].nameWithOwner' |\nwhile read -r repo; do\n  name=\"${repo//\\//_}\"\n  git clone --mirror --quiet \"https://github.com/${repo}.git\" \"${name}.git\"\n  gitleaks git \"${name}.git\" --no-banner --redact \\\n    --report-format json --report-path \"${name}.json\" || echo \"LEAKS: ${repo}\"\n  rm -rf \"${name}.git\"\ndone\n```\n\n### Enforce on the server side\n\nClient hooks and CI both run after code leaves the laptop. On a self-hosted Git server, a `pre-receive` hook that runs `gitleaks git --log-opts` against the pushed range rejects the push itself, so the secret never lands in the central repository. Hosted platforms offer a similar control through push protection, and you can run both.\n\n### Other practical upgrades\n\n- Emit SARIF with `--report-format sarif` when you want findings to appear in a code scanning interface instead of a step summary.\n- Pin CI dependencies. GitHub opened workflow dependency lockfiles in public preview on October 14, 2026, which is worth testing on a non-critical repository.\n- Share one org-wide config by pointing `[extend]` at a path, or by setting`GITLEAKS_CONFIG` in CI. Keep repo-specific allowlists in each repo.\n- Use `init.templateDir` or a bootstrap script so new clones receive the hook automatically.\n- Run the same binary on GitLab CI, Jenkins or CircleCI. Gitleaks is a CLI, so the commands in Step 11 translate directly.\n- Review the tracker before upgrading. Open issues in October 2026 include a request to set `--redact` through the config file, which does not exist yet, so keep the flag on the command line.\n\n## Frequently Asked Questions\n\n### Is Gitleaks free for commercial use?\n\nYes. The project uses the MIT license, which permits commercial use, and the pre-commit framework is also free. Your only costs are CI minutes and the time you spend tuning rules.\n\n### Does Gitleaks scan the full Git history?\n\nIt does when you run `gitleaks git` without range options. Add `--log-opts` to limit the scan to a commit range, or `--staged` to inspect only staged changes. The `dir` command ignores history entirely.\n\n### How do I stop false positives without hiding real leaks?\n\nUse the narrowest tool that works. Tighten the rule regex first, then raise the entropy floor, then add a path allowlist with `targetRules` so the suppression applies to one rule only. Reserve `gitleaks:allow` comments and fingerprints for individual, documented cases.\n\n### Should I use Gitleaks or TruffleHog?\n\nThey overlap, and many teams run one as a fast gate and consider the other for triage. Gitleaks favors a simple binary and rules you control. TruffleHog is commonly chosen when checking whether a found credential is still active matters. Test both on a copy of your own repositories, because rule coverage varies by credential type.\n\n### Does GitHub secret scanning make Gitleaks unnecessary?\n\nNot for most teams. GitHub scanning works only on GitHub, and some of its security features need a paid license. Gitleaks runs on any host, on a laptop and in any CI system, and it understands the custom token formats you define. Running both is a reasonable belt-and-braces setup.\n\n### What should I do after a secret reaches the main branch?\n\nRotate the credential first, then review provider logs for misuse, then clean history if your policy requires it. Deleting the commit alone does not protect you, because clones and forks keep the old data.\n\n### How often should I update the Gitleaks version?\n\nCheck the releases page monthly and update the pinned version in both `.pre-commit-config.yaml` and the workflow. After each bump, run the self-test and compare findings against the previous version so a rule change does not surprise you.", "url": "https://wpnews.pro/news/gitleaks-tutorial-12-steps-60-min-2026", "canonical_source": "https://shattered.io/gitleaks-tutorial-secret-scanning-pre-commit-github-actions-12-steps-2026/", "published_at": "2026-10-28 20:25:37+00:00", "updated_at": "2026-10-28 20:44:13.034036+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents"], "entities": ["Gitleaks", "GitHub", "TruffleHog", "GitHub Actions", "Cursor CLI", "The New Stack", "Shattered"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/gitleaks-tutorial-12-steps-60-min-2026", "markdown": "https://wpnews.pro/news/gitleaks-tutorial-12-steps-60-min-2026.md", "text": "https://wpnews.pro/news/gitleaks-tutorial-12-steps-60-min-2026.txt", "jsonld": "https://wpnews.pro/news/gitleaks-tutorial-12-steps-60-min-2026.jsonld"}}