Ignore Secrets in Git

Su Suriya Ravichandran Updated 10 Nov 2026
4 min read ·Lesson 173 of 200

Version Control Remembers Everything

A version control system records every change, and it keeps every copy of every change for as long as the repository exists. That is what makes it valuable, and it is also why a secret committed once is a secret published forever. Deleting the file in the next commit does not remove the key from the history, and every clone of the repository carries it. Lesson 171 explained what to do when a key leaks. This lesson explains how to stop the leak from happening, using three layers: an ignore file that stops secret files from being added, a check on what is staged before a commit, and a scan of the contents, which catches a secret placed in a file with an innocent name.


The Ignore File

The version control system can be told to ignore certain paths, using a file conventionally named .gitignore. Listed paths are never staged by a normal add, which prevents the most common mistake: a real .env file committed alongside the code:

# secrets and local settings: never commit these
.env
.env.*
!.env.example
*.pem
id_rsa*
credentials*
service-account*.json

Two details matter. The !.env.example line is an exception: it re-includes the template, which is meant to be committed (Lesson 172). And the patterns must be broad enough to catch variants, such as .env.local or service-account-prod.json, because a narrow pattern covers only the name someone expected.

An ignore file is a convenience, not a guarantee. A file can be added with an explicit flag that overrides the ignore rules, and a file that was committed before it was ignored stays tracked. The ignore file prevents accidents, and the checks below catch the rest.


Checking What Is About to Be Committed

Before a commit is created, the staged files can be checked against the same rules, by name and by content. A check that runs automatically before each commit, usually through a pre-commit hook, stops a risky commit before it exists. The function below applies both checks to a set of staged files:

"""staged_check.py: refuse a commit that stages secret-like files or secret-like content."""

import re
from fnmatch import fnmatch
from os.path import basename

RISKY_NAMES = [".env", ".env.*", "*.pem", "id_rsa*", "credentials*", "service-account*.json"]
ALLOWED_NAMES = {".env.example"}
KEY_PATTERN = re.compile(r"sk-ant-[A-Za-z0-9_-]{10,}")


def risky_by_name(paths: list[str]) -> list[str]:
    flagged = []
    for path in paths:
        name = basename(path)
        if name in ALLOWED_NAMES:
            continue
        if any(fnmatch(name, pattern) for pattern in RISKY_NAMES):
            flagged.append(path)
    return flagged


def risky_by_content(staged: dict[str, str]) -> list[str]:
    return [path for path, text in staged.items() if KEY_PATTERN.search(text)]


if __name__ == "__main__":
    paths = ["src/app.py", ".env", ".env.example", "keys/prod.pem",
             "service-account-prod.json", "README.md"]
    print("risky names:", risky_by_name(paths))

    staged = {"notes.txt": "rotate this before Friday: sk-ant-api03-ExampleValue42",
              "app.py": "print('hello')"}
    print("risky content:", risky_by_content(staged))

Output:

risky names: ['.env', 'keys/prod.pem', 'service-account-prod.json']
risky content: ['notes.txt']

How It Works

  1. The name check uses the file's base name, so a secret file inside a folder is caught as well as one at the top level.
  2. The template is allowed by name. .env.example matches the .env.* pattern, and the explicit allowance keeps it from being flagged, which mirrors the exception in the ignore file.
  3. Patterns use shell-style matching, so *.pem and service-account*.json catch whole families of names.
  4. The content check catches a secret in an innocent file. notes.txt has a harmless name, and its content contains a key, which the name check cannot see.
  5. Both checks return lists, so a commit hook can print every problem at once and stop the commit, leaving the decision to remove the secret to the developer.

When a Secret Is Already Committed

If a secret has been committed, the damage is done once the commit has been pushed, and removing the file from the next commit is not enough. The steps are the same as in Lesson 171: revoke and replace the key first, because the old value may already be copied. Then remove it from the history. A history-rewriting tool can remove a file or a string from every commit, and after rewriting, the repository must be force-pushed and every collaborator must re-clone or reset. Forced rewriting breaks other people's work if it is done carelessly, so it should be coordinated, not done alone at night.

Rewriting history is cleanup, not protection. A key that was public for any time must be treated as compromised, whatever the repository looks like afterward.


Hooks and Teams

A check that depends on each developer remembering to run it is a check that will sometimes not run. Placing the check in a hook that the version control system runs automatically before each commit makes it the default. Hooks are local, so the team should also run the same check in the continuous integration system, which runs on every push and cannot be skipped by a local setting. The two layers together catch a secret that a developer bypassed locally.


Summary

Version control keeps every change, so a committed secret stays exposed in the history. An ignore file prevents secret files from being added, and an exception keeps the template in the repository. A pre-commit check finds risky names and risky content in the staged files, including secrets placed in innocuously named files. A committed secret must be revoked and replaced first, and only then removed from history, with the team coordinated. The next lesson moves from the secrets themselves to the input the application receives from its users.

0 Comments

Reviewed before they appear

No comments yet.

Anthropic SDK Using Python
Anthropic SDK Intro Claude API Intro SDK Requirements Install Anthropic SDK Upgrade the SDK Create an API Key Set API Keys Use Environment Variables Import Anthropic Create a Client Send Your First Message Choose a Claude Model Set Max Tokens Read a Response Print Response Text Understand Message Blocks Use System Prompts Send User Messages Set Temperature Handle Stop Reasons Messages API Intro Create Messages Use Message Roles Send Conversation History Continue a Conversation Build a Chat Loop Reset Chat History Use Multiple Turns Inspect Message IDs Read Usage Data Write Clear Prompts Set a System Prompt Add Context to Prompts Use Prompt Templates Add Examples to Prompts Request JSON Output Use XML Tags Control Response Length Improve Prompt Quality Avoid Prompt Injection Read Text Blocks Read Content Blocks Check Stop Reasons Count Input Tokens Count Output Tokens Count Tokens Before Sending Limit Output Tokens Use Stop Sequences Read Request IDs Parse API Responses Streaming Intro Stream Text Output Use Stream Events Use Stream Helpers Get Final Messages Stream Async Responses Handle Stream Errors Build a Live Chat Stream Long Responses Close Streams Safely Build a Chatbot Keep Chat History Manage Chat Sessions Add Chat Memory Build a CLI Chatbot Build a Web Chatbot Use Flask with Claude Use FastAPI with Claude Return JSON from an API Add Chat Error Messages Async SDK Intro Create an Async Client Send Async Messages Use asyncio Run Parallel Requests Stream Async Text Handle Async Errors Set Async Timeouts Close Async Clients Choose Sync or Async Tool Use Intro Define a Tool Create Tool Schemas Send Tools to Claude Read Tool Calls Run Python Tools Return Tool Results Continue After Tool Use Control Tool Choice Handle Tool Errors Use the beta_tool Decorator Create a Tool Runner Add Tool Docstrings Validate Tool Inputs Use Multiple Tools Limit Tool Calls Log Tool Activity Build a Weather Tool Build a Search Tool Build a Calculator Tool Structured Output Intro Request JSON Data Validate JSON Output Use Pydantic Models Handle Invalid JSON Extract Fields from Text Generate Typed Data Use Image URLs Send Base64 Images Read PDFs with Claude Use Output Schemas Build Data Extraction Test Structured Responses Send Image Inputs Upload Files Use File IDs Process Text Files Summarize Documents Extract Table Data Handle File Errors Read Cache Usage Reduce Repeated Input Costs Debug Cache Misses Prompt Caching Intro Enable Prompt Caching Cache System Prompts Cache Long Context Test Cache Performance Use Cache Breakpoints Cache Best Practices Message Batches Intro Create a Message Batch Set Batch IDs Check Batch Status List Message Batches Get Batch Results Process Batch Outputs Handle Failed Batch Items Use Batches for Bulk Tasks Compare Batches and Requests Handle API Errors Handle Rate Limits Retry Failed Requests Set Request Timeouts Handle Network Errors Use Exponential Backoff Build Reliable API Calls Use Extended Thinking Log API Errors Track Request IDs Paginate API Results Set Thinking Budgets Read Thinking Blocks Use Tool Search Use Web Search Tools Use Computer Tools Use Citations Use Token Counting Read Raw Responses Set Custom Headers Use Claude on AWS Bedrock Use Claude on Vertex AI Use Claude on Microsoft Foundry Configure Cloud Credentials Choose a Cloud Provider Compare API Integrations Configure Provider Clients Handle Cloud Errors Use Cloud SDK Extras Deploy Cloud Apps Protect API Keys Use a .env File Ignore Secrets in Git Validate User Input Reduce Prompt Injection Limit Tool Permissions Avoid Unsafe Tool Calls Sanitize Tool Results Set Usage Limits Review Security Logs Test SDK Functions Mock API Responses Test Error Handling Test Streaming Code Measure Token Usage Track API Costs Configure Logging Deploy with Docker Deploy a FastAPI App Monitor Production Apps Build a Claude Chatbot Build a Text Summarizer Build a Code Explainer Build a JSON Extractor Build a PDF Assistant Build a Streaming Chat App Build a Tool-Calling Agent Build a Batch Processor Build a Research Assistant Build a Claude API Dashboard
Ask about this post
AI Ask about this post

Ask questions about Ignore Secrets in Git and get answers drawn from it.

Signed-in readers only.