Cursor vs VS Code Secrets Guard: 13 Steps, 40 Min [2026]

Developers keep asking one question in the cursor vs vscode debate that feature tables skip: which editor keeps your .env file away from the AI agent? Stack Overflow’s 2025 Developer Survey found VS Code used regularly by 75.9% of all respondents and Cursor by 17.9%. With that many people running agents inside their repositories, a stray database URL or private key in the workspace is a real exposure path, and nobody should leave it to luck.

The two editors answer the problem differently. Cursor ships a .cursorignore file that hides paths from Agent, Tab and @ mentions. In the VS Code documentation pages I checked, no file does that job. VS Code leans on approval gates, workspace-limited file access and sandboxing instead, plus one setting, chat.tools.edits.autoApprove, that forces manual approval before the agent edits files such as .env. Neither approach is complete on its own, and each vendor says so in its own docs.

This tutorial builds secretsguard, a single Python script that keeps one list of sensitive patterns and turns it into four things: a managed .cursorignore block, a VS Code edit gate, a coverage audit that fails CI when the two drift apart, and a pre-commit scanner for leaked keys. It takes about 40 minutes. Our Workspace Trust audit covers the layer where VS Code decides whether agents run at all. This guide works one layer down, at the level of individual files.

A note on how I tested. I ran every script below on Python 3.12.3 and Git 2.43.0, and the terminal output in this article comes from those real runs. Editor behavior comes from the Cursor and VS Code documentation fetched on October 28, 2026, so repeat the canary tests in Steps 12 and 13 on your own machine before you trust the result.

Google · Preferred Sources

Don't miss new tech stories on Google

Add Tech Insider once in the Google app and our stories appear in your news suggestions.

Add Now

Why Secrets Leak Into AI Context in 2026

Secrets reach an AI agent through five paths, and each one needs a different control. Most teams only plug the first.

The first path is a direct file read. The agent opens .env with its file tool, or you drag it into the chat with an @ mention. The second path is the terminal. An agent that wants to debug a failing connection can run cat .env or printenv and read the answer back. The third path is tools. A filesystem MCP server, a database client or a browser tool can return secret content as a normal tool result. The fourth path is the edit. An agent that rewrites a config file may print the old and new values in its diff, which lands in the chat history. The fifth path is the commit, where a real key reaches Git history and every later clone, CI job and cloud agent sees it.

Scale makes the defaults matter. Gartner predicted that 90% of enterprise software engineers would use AI code assistants by 2028, up from less than 14% in early 2024, as GitHub’s write-up of the 2025 Magic Quadrant reports. When most engineers work next to an agent, one engineer’s careless .env becomes the team’s incident.

Both vendors warn against treating their controls as airtight. Cursor’s documentation says that although it blocks ignored files, complete protection is not guaranteed because language models behave unpredictably. The VS Code security guide says agent sandboxing is an added layer and not a virtual machine, a user-account boundary or a standalone security boundary. It also says auto-approval rules rely on best-effort command parsing, with known gaps around shell aliases, quote concatenation and complex shell syntax.

That honesty shapes the design here. You get four layers. First, keep real secrets out of the workspace whenever you can. Second, block or gate the files that must stay. Third, scan what you commit. Fourth, constrain the shell with a sandbox. The script automates layers two and three, and the later steps show how to wire in the other two.

Leak pathBest controlWhere it lives in this project
Agent reads a file.cursorignore in Cursor, workspace scoping in VS CodeStep 5 and Step 6
Agent runs a shell commandAgent sandboxing and manual terminal approvalStep 7 and Step 13
MCP or other tool returns contentServer review, stdio sandboxing, no secrets in the folderStep 12
Agent edits a sensitive filechat.tools.edits.autoApprove set to false by globStep 7
Secret reaches Git historyPre-commit scan plus CI scanSteps 10, 11 and 13

Cursor vs VS Code in October 2026: What Changed

Both editors moved fast in the last few weeks, and several changes touch this topic.

On the VS Code side, Microsoft announced a modernized VS Code UI on October 15, 2026, available as a preview, with the classic interface kept for now. Microsoft says the classic UI is planned for eventual retirement but gave no date as of October 28. The Agents window is already available in Stable as a preview, which pulls agent sessions, progress and review into one place. Menu names in this guide follow the current docs, so expect small label differences if you run the new UI.

On the Cursor side, the release notes show cloud agents gaining interactive canvases in mid-October 2026, and an Environments API in beta that supports up to 300 repositories. The model picker now lists Claude Opus 5.5, Claude Sonnet 5.5 and Grok 4.7, and Claude Haiku 5.5 is available as an opt-in model you switch on under Cursor Settings > Models. Cursor also reorganized its plans on October 12, 2026 around The X Subscription, with Free, Plus ($8 a month), Super ($100) and Ultra ($200) tiers listed in its release notes. Nothing I read ties ignore-file support to a plan tier.

Why does any of this matter for secrets? Because cloud agents and cloud sessions run somewhere other than your laptop. The VS Code docs state plainly that local approval and sandbox settings do not configure the cloud service, and they mark cloud sessions as experimental. If you hand a repository to a remote agent, whatever sits in that repository goes with it. A tracked .env file is the worst case, and the pre-commit scanner in Step 11 exists for exactly that moment.

More models also means more places for file content to flow. Every model you switch on in Cursor, and every harness you pick in VS Code, handles instructions and context its own way. A control that lives in the file system, such as an ignore file or a pre-commit hook, stays the same no matter which model answers.

Prerequisites and Versions

You need a machine where you can run a terminal, a Git repository and both editors. The table lists what I used and what each piece does.

ComponentVersion usedWhy you need it
CursorCurrent stable release (check Help > About)Reads .cursorignore, hosts the Indexing settings
VS Code1.140.0 or newerSupports the chat.tools.edits.autoApprove setting and the Agents window
Python3.12.3 (tested)Runs secretsguard.py, which uses only the standard library
Git2.43.0 (tested)Powers check-ignore and the pre-commit hook
GitHub Copilot sign-inActive accountLets VS Code run agent sessions for the canary test
Shellbash, zsh or Git BashRuns the commands as written

Microsoft’s October 1, 2026 open source post on OpenAPI tooling references VS Code 1.140.0, which is why I treat that build as the floor. Newer builds should behave the same for this tutorial, but I only verified the setting names against the current docs.

Cursor ships often, so I list no fixed build for it. One report put the stable release at 3.27.6 on October 8, 2026, but your About dialog is the authority.

Nothing here needs a package install. The script imports argparse, json, re, subprocess and pathlib, all from the standard library. That keeps the tool easy to audit, which matters for a tool that touches your secrets.

If you already keep editor settings in sync across machines, our guide to Cursor and VS Code shared config sync pairs well with this one, because the VS Code gate you generate in Step 7 is a workspace setting you can commit and share.

One rule before you start. Use fake secrets only. Every value in this tutorial is invented, and the one AWS-style key is the placeholder example that AWS itself publishes in its documentation. Never paste a real credential into a test file, even briefly, because editor history, chat transcripts and cloud sessions can keep it.

How Each Editor Protects Sensitive Files

The table below compares the controls side by side. Read it before you build anything, because it explains why one pattern list has to feed two different mechanisms.

QuestionCursorVS Code
Can I hide a file from agent reads?Yes, with .cursorignore (gitignore syntax). It covers Agent, Tab and @ mentionsNo per-file read block in the docs I checked. Agent file tools stay inside the workspace folder
Can I force approval for edits by path?Indirectly, since ignored files are not accessible to AgentYes, chat.tools.edits.autoApprove with glob patterns set to false
Does it stop terminal commands?No. Terminal tools sit outside what the ignore file can blockPartly. Agent sandboxing confines shell commands, and approvals gate them
Does it stop MCP tools?No. MCP tools used by Agent sit outside the ignore fileReview servers before trusting them. Stdio servers can be sandboxed on macOS and Linux
Any defaults?Ignores .gitignore matches plus a default list that includes .env* and lockfilesNo default secret-file list documented
User-level scopeA global ignore list in user settings, empty by default, plus a hierarchical toggleUser settings and profiles, with session-level approvals recommended
Remote runsCloud agents and Environments API (beta)Cloud sessions are experimental, and local settings do not govern them
Vendor caveatComplete protection not guaranteedSandbox is an added layer, not a standalone security boundary

Read the table as a division of labor. Cursor blocks reads at the file level and VS Code gates edits at the file level. Neither one stops an agent from running a shell command that prints a file. That gap is the main reason the project includes a scanner and a canary test, and why the advanced section pushes you to keep real secrets out of the workspace entirely.

Privacy is a separate question from file access. Cursor’s Privacy Mode and VS Code’s telemetry settings decide what a vendor may retain, not which files an agent may open. Our network and telemetry audit measures that second question with a proxy, and you should run it after this tutorial so you know both what the agent can read and what leaves your machine.

One more difference deserves a mention. Cursor ignores whatever your .gitignore already lists, so a repo with a tidy .gitignore gets partial protection for free. VS Code does not read .gitignore for this purpose in anything I found. If your team uses both editors, a .gitignore-only strategy leaves the VS Code half uncovered. The shared pattern list fixes that.

Steps 1-3: Create the Project and Plant Fake Secrets

Step 1: Create the repository and the test fixtures

Start with a throwaway repo. The fixtures include real-looking file names so the matcher gets a fair test: a root .env, a safe .env.example, a nested .env.local, a PEM file, and a source file with a placeholder AWS key.

mkdir secretsguard-demo && cd secretsguard-demo
git init -q .
mkdir -p src infra tests services/api .vscode

printf 'DB_URL=postgres://app:hunter2@db:5432/app\n' > .env
printf 'DB_URL=postgres://user:pass@localhost/app\n' > .env.example
printf 'STRIPE_SECRET="sk_live_abcdefghijklmnopqrstuvwx"\n' > .env.production
printf 'PORT=8080\n' > services/api/.env.local
printf -- '-----BEGIN RSA PRIVATE KEY-----\nMIIB...fake\n-----END RSA PRIVATE KEY-----\n' > infra/deploy.pem
printf 'const key = "AKIAIOSFODNN7EXAMPLE";\n' > src/aws.js
printf 'print("hello")\n' > src/app.py

Expect no output. If git init complains about a safe directory or an existing repo, you ran it inside another project. Move to a clean folder first.

Step 2: Confirm your tool versions

Check Python and Git before you write code. A very old Python fails on the f-strings the script uses, and an old Git may lack the check-ignore options from Step 9.

python3 --version
git --version
Python 3.12.3
git version 2.43.0

On Windows, run these in Git Bash or WSL so the shell syntax in later steps works unchanged. PowerShell users should translate the printf lines by hand.

Step 3: Define the sensitive patterns once

The whole project rests on one list. Open a new file named secretsguard.py and start with the constants. Everything else in the script reads from these names.

SENSITIVE = [
    "**/.env", "**/.env.*", "**/*.pem", "**/*.key", "**/id_rsa",
    "**/credentials.json", "**/secrets.json", "**/*.tfstate", "**/*.tfvars",
]
SAFE = ["**/.env.example"]

Three decisions hide in those lines. The **/ prefix makes every pattern match at any depth, including the repository root. The .env.* pattern catches .env.production and .env.local, but it also catches .env.example, which you want agents to read because it documents the variables without holding values. The SAFE list carves that file back out. The list also includes Terraform state and variable files, since terraform.tfstate routinely holds plaintext credentials.

The patterns follow the examples in Cursor’s docs, which suggest environment files, credential files and key files for a global ignore list. Add the formats your stack uses, such as *.p12, *.keystore or kubeconfig.

Steps 4-6: Build the Matcher and the Cursor Side

Step 4: Write a glob matcher you can test

Python’s PurePath.match treats inconsistently across versions, so write a small translator that turns a glob into a regular expression. The rules are short: / means any number of directories (including none), * means anything except a slash, and ? means one character that is not a slash.

import re

def glob_to_regex(glob):
    out, i = "", 0
    while i < len(glob):
        if glob.startswith("**/", i):
            out += "(?:.*/)?"; i += 3
        elif glob[i] == "*":
            out += "[^/]*"; i += 1
        elif glob[i] == "?":
            out += "[^/]"; i += 1
        else:
            out += re.escape(glob[i]); i += 1
    return re.compile("^" + out + "$")

def matches(path, globs):
    return any(glob_to_regex(g).match(path) for g in globs)

def is_sensitive(path):
    return matches(path, SENSITIVE) and not matches(path, SAFE)

Now pin the behavior with tests. The edge cases that bite are a root-level .env and a nested .env.production, because a naive translator misses one or the other. Save the seven tests from the project section as tests/test_secretsguard.py, add an empty tests/__init__.py, and run them.

touch tests/__init__.py
python3 -m unittest discover -s tests -t . -v
test_example_file_is_safe (tests.test_secretsguard.GlobTests.test_example_file_is_safe) ... ok
test_matches_any (tests.test_secretsguard.GlobTests.test_matches_any) ... ok
test_nested_env_matches (tests.test_secretsguard.GlobTests.test_nested_env_matches) ... ok
test_pem_anywhere (tests.test_secretsguard.GlobTests.test_pem_anywhere) ... ok
test_root_level_env_matches (tests.test_secretsguard.GlobTests.test_root_level_env_matches) ... ok
test_source_file_not_sensitive (tests.test_secretsguard.GlobTests.test_source_file_not_sensitive) ... ok
test_star_does_not_cross_slash (tests.test_secretsguard.GlobTests.test_star_does_not_cross_slash) ... ok

Ran 7 tests in 0.001s

OK

If you see ImportError: Start directory is not importable, the empty tests/__init__.py is missing. That exact error cost me a minute during the build.

Step 5: Generate a managed .cursorignore block

The script writes its patterns between two marker comments. On every run it removes the old block and writes a fresh one, so you can add your own lines above or below without losing them.

BEGIN, END = "# >>> secretsguard >>>", "# <<< secretsguard <<<"

block = "\n".join([BEGIN, *SENSITIVE, *("!" + s for s in SAFE), END]) + "\n"
old = re.sub(re.escape(BEGIN) + ".*?" + re.escape(END) + "\n?", "", old, flags=re.S)
f.write_text(old + block)

Run python3 secretsguard.py init once the full script from the project section is in place. The .cursorignore that appears looks like this:

# >>> secretsguard >>>
**/.env
**/.env.*
**/*.pem
**/*.key
**/id_rsa
**/credentials.json
**/secrets.json
**/*.tfstate
**/*.tfvars
!**/.env.example
# <<< secretsguard <<<

The file uses gitignore syntax, as Cursor’s docs specify. The final ! line un-ignores the example file. Commit .cursorignore so every teammate gets the same protection without touching their settings.

Step 6: Set the global ignore list and the hierarchical toggle

Project files only protect projects that have them. A user-level list covers every repo you open, including the ones you cloned five minutes ago.

Open Cursor Settings > Indexing > Ignore Files. That is where the docs place the Hierarchical Cursor Ignore toggle, which makes Cursor search parent directories for .cursorignore files. Turn it on if you work in monorepos with nested packages. Then look for the global ignore list in the same area. If your build puts it somewhere else, search the settings for the word ignore. The list is empty by default, so paste in the patterns from Step 3:

**/.env
**/.env.*
**/credentials.json
**/secrets.json
**/*.key
**/*.pem
**/id_rsa

Cursor already ignores .env*, lockfiles and a long list of binary formats by default, per the same docs, so the global list mostly documents your intent and adds key and credential files. Writing it down still pays off, because defaults can change between releases.

If your team shares agent instructions across both editors, say in the shared file that agents must never print environment values. Our walkthrough of shared AI rules for Cursor and VS Code on one repo shows how to keep one instruction file for both. Treat that line as a request to the model and not as a block. Only the ignore file and the gates enforce anything.

Steps 7-9: Add the VS Code Gate and Audit Coverage

Step 7: Merge an edit gate into .vscode/settings.json

VS Code’s security guide recommends configuring chat.tools.edits.autoApprove with glob patterns, for example "**/.env": false, so edits to sensitive files need manual approval. The script builds that map from the same SENSITIVE list, which keeps the two editors in step.

def load_settings(root):
    f = root / ".vscode" / "settings.json"
    if not f.exists():
        return {}
    try:
        return json.loads(f.read_text())
    except json.JSONDecodeError:
        sys.exit("settings.json has comments or a syntax error. Merge by hand, then rerun.")

settings = load_settings(root)
gate = settings.setdefault("chat.tools.edits.autoApprove", {})
gate.update({g: False for g in SENSITIVE})

The guard in load_settings matters. VS Code allows comments in settings.json, but Python’s json module does not. Without the guard, the script would fail halfway or, worse, rewrite a file with your comments stripped. With it, a commented file stops the run cleanly:

settings.json has comments or a syntax error. Merge by hand, then rerun.

After init on a clean settings file, you get this:

{
  "chat.tools.edits.autoApprove": {
    "**/.env": false,
    "**/.env.*": false,
    "**/*.pem": false,
    "**/*.key": false,
    "**/id_rsa": false,
    "**/credentials.json": false,
    "**/secrets.json": false,
    "**/*.tfstate": false,
    "**/*.tfvars": false
  }
}

Pair this gate with agent sandboxing. The gate controls edits made through VS Code’s edit tools, while the sandbox confines shell commands and their child processes at the operating-system level. Our VS Code agent sandbox setup walks through turning it on. One default to know: the VS Code docs say outbound network access stays enabled because chat.agent.sandbox.network.allowNetwork defaults to true, so restrict the network separately if you want a tighter box.

Step 8: Run the coverage audit

The audit walks the repository, finds every file that matches the sensitive list, and checks whether both editors’ controls cover it. Run it before init to see the gaps, then again after to confirm they close.

python3 secretsguard.py audit
.env                         cursor=GAP vscode=GAP
.env.production              cursor=GAP vscode=GAP
infra/deploy.pem             cursor=GAP vscode=GAP
services/api/.env.local      cursor=GAP vscode=GAP
gaps: 8

Eight gaps: four sensitive files, two editors each. Notice that .env.example does not appear, because the SAFE list exempts it. Now run init and audit again with --strict, which turns any gap into a non-zero exit code:

python3 secretsguard.py init
python3 secretsguard.py audit --strict
.env                         cursor=ok  vscode=ok 
.env.production              cursor=ok  vscode=ok 
infra/deploy.pem             cursor=ok  vscode=ok 
services/api/.env.local      cursor=ok  vscode=ok 
gaps: 0

In a fresh copy with only a .env and no init, --strict printed gaps: 2 and exited with status 1. That exit code is what lets CI fail a pull request that adds a new secret file type without updating the controls.

Step 9: Cross-check with git check-ignore

Don’t trust your own matcher alone. Cursor’s docs recommend testing ignore patterns with git check-ignore -v. Because .cursorignore uses gitignore syntax, you can point Git at it as an excludes file and let Git’s own engine judge each path:

git -c core.excludesFile=.cursorignore check-ignore -v --no-index \
  .env services/api/.env.local .env.example src/app.py
.cursorignore:2:**/.env	.env
.cursorignore:3:**/.env.*	services/api/.env.local
.cursorignore:11:!**/.env.example	.env.example

Each line names the file, line number and pattern that matched. The first two paths match deny patterns. The third matches the negation on line 11, which means the example file stays visible. src/app.py prints nothing because no pattern touches it. If Git’s verdict ever disagrees with the audit, trust Git, then fix glob_to_regex and add a test for the case.

Steps 10-11: Add the Content Scanner and a Pre-Commit Hook

Step 10: Scan file contents for key patterns

File names catch the obvious cases. Contents catch the mistakes, like a key pasted into a source file. The scanner runs three regular expressions: an AWS access key ID, a PEM private key header, and a generic quoted token assignment.

CONTENT_RULES = {
    "aws-access-key": re.compile(r"AKIA[0-9A-Z]{16}"),
    "private-key": re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"),
    "generic-token": re.compile(r"(?i)(api[_-]?key|token|secret)\s*[:=]\s*['\"][A-Za-z0-9_\-]{20,}['\"]"),
}

Run it across the working tree:

python3 secretsguard.py scan
.env.production:1: generic-token
infra/deploy.pem:1: private-key
src/aws.js:1: aws-access-key
findings: 3

The scanner reports a file, a line and a rule name, and it never prints the matched value. That choice is deliberate. A scanner that echoes secrets into terminal scrollback, CI logs or an agent’s chat window defeats its own purpose. The exit status is 1 whenever it finds something, so any script or hook can act on it.

Regular expressions have limits. The generic rule misses short tokens and flags some harmless long strings. For production use, pair this script with a dedicated scanner such as gitleaks or trufflehog in CI. This one exists to give you a fast local gate and to show how the pieces connect.

Step 11: Install the pre-commit hook

A hook runs the scanner on staged files, so a secret stops at git commit instead of reaching history.

python3 secretsguard.py hook
git add src/aws.js src/app.py secretsguard.py
git commit -m "test"
installed .git/hooks/pre-commit
src/aws.js:1: aws-access-key
findings: 1

Git aborts the commit, and the key never enters history. The hook scans only added, copied or modified staged files, which keeps it fast on large repos. The script skips its own file and the tests/ folder, since they contain pattern strings that would trigger the rules.

Two limits apply. Anyone can bypass a local hook with git commit --no-verify, so treat it as a seatbelt and keep the CI check from Step 13 as the real gate. Hooks also live in .git/hooks, which Git does not share, so each teammate runs python3 secretsguard.py hook once after cloning. Cursor has its own hooks feature for agent lifecycle events, which is a different mechanism. Our guide to building Cursor hooks as a VS Code extension explains how those work, and they complement a Git hook well.

Steps 12-13: Canary Tests in Both Editors, Then CI

Step 12: Test Cursor with a canary value

A canary is a unique fake string you plant in a protected file. If the agent can ever repeat it back, the control failed. Add one to the .env:

printf 'CANARY_VALUE=zebra-quartz-7731\n' >> .env

Open the folder in Cursor and run three checks in Agent chat.

  1. Ask the agent to summarize what .env contains. With .cursorignore active, it should report that it cannot access the file, and it should never produce the canary string.
  2. Type @.env in the chat. The ignored file should not resolve as a reference, because the docs say ignored files are blocked from @ mention references.
  3. Ask the agent to run cat .env in the terminal. This is the check that matters most. Cursor’s documentation says the terminal and MCP tools Agent uses sit outside what .cursorignore can block, so a command the agent runs may well print the canary if you approve it. Deny it. Better, remove real secrets from the workspace so the question never arises.

Result three tells you where your real risk lives. If you also run filesystem or database MCP servers, repeat the test through those tools. Our guide to Cursor MCP servers versus VS Code agents covers configuring both sides, and it is worth reading before you give any server access to a folder that holds environment files.

Step 13: Test VS Code, then wire the audit into CI

Open the same folder in VS Code with the workspace trusted and a Copilot agent session available. Run two checks.

  1. Ask the agent to add a new variable to .env. Because **/.env is set to false in chat.tools.edits.autoApprove, VS Code should ask for manual approval instead of applying the edit. If it applies silently, a broader approval rule or a session-level grant is overriding your gate. Check Chat permissions and reset them.
  2. Ask the agent to read the file. The docs I checked describe no per-file read block, so expect the agent may be able to read .env. That result is the reason the next layers exist, so don’t skip them.

The VS Code security guide recommends granting tool and terminal permissions at the session level, and not at the workspace or user level, to limit how long elevated trust lasts. Follow that advice here.

Last, put the audit and scanner in CI so the controls cannot rot. This GitHub Actions workflow fails any pull request that adds an unprotected sensitive file or a recognizable key:

name: secretsguard
on: [pull_request]
jobs:
  guard:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: python3 secretsguard.py audit --strict
      - run: python3 secretsguard.py scan

Note that audit only sees files that exist in the checkout. Untracked local .env files never reach CI, which is correct, since the job guards the repository and not your laptop.

The Complete Working Project

Here is the file tree, followed by the full script. Copy secretsguard.py as shown and the tests below it, and every command in this tutorial runs as written.

secretsguard-demo/
  secretsguard.py
  .cursorignore            (generated)
  .vscode/settings.json    (generated)
  .git/hooks/pre-commit    (generated)
  tests/
    __init__.py
    test_secretsguard.py
  .github/workflows/secretsguard.yml
#!/usr/bin/env python3
"""secretsguard: keep secrets out of AI context in Cursor and VS Code."""
import argparse, json, re, subprocess, sys
from pathlib import Path

SENSITIVE = [
    "**/.env", "**/.env.*", "**/*.pem", "**/*.key", "**/id_rsa",
    "**/credentials.json", "**/secrets.json", "**/*.tfstate", "**/*.tfvars",
]
SAFE = ["**/.env.example"]
SKIP_DIRS = {".git", "node_modules", ".venv", "__pycache__"}
BEGIN, END = "# >>> secretsguard >>>", "# <<< secretsguard <<<"
CONTENT_RULES = {
    "aws-access-key": re.compile(r"AKIA[0-9A-Z]{16}"),
    "private-key": re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"),
    "generic-token": re.compile(r"(?i)(api[_-]?key|token|secret)\s*[:=]\s*['\"][A-Za-z0-9_\-]{20,}['\"]"),
}


def glob_to_regex(glob):
    out, i = "", 0
    while i < len(glob):
        if glob.startswith("**/", i):
            out += "(?:.*/)?"; i += 3
        elif glob[i] == "*":
            out += "[^/]*"; i += 1
        elif glob[i] == "?":
            out += "[^/]"; i += 1
        else:
            out += re.escape(glob[i]); i += 1
    return re.compile("^" + out + "$")


def matches(path, globs):
    return any(glob_to_regex(g).match(path) for g in globs)


def is_sensitive(path):
    return matches(path, SENSITIVE) and not matches(path, SAFE)


def walk(root):
    for p in sorted(root.rglob("*")):
        if p.is_file() and not (set(p.relative_to(root).parts) & SKIP_DIRS):
            yield p.relative_to(root).as_posix()


def cursor_patterns(root):
    f = root / ".cursorignore"
    if not f.exists():
        return [], []
    lines = [l.strip() for l in f.read_text().splitlines() if l.strip() and not l.startswith("#")]
    return [l for l in lines if not l.startswith("!")], [l[1:] for l in lines if l.startswith("!")]


def load_settings(root):
    f = root / ".vscode" / "settings.json"
    if not f.exists():
        return {}
    try:
        return json.loads(f.read_text())
    except json.JSONDecodeError:
        sys.exit("settings.json has comments or a syntax error. Merge by hand, then rerun.")


def cmd_init(root):
    block = "\n".join([BEGIN, *SENSITIVE, *("!" + s for s in SAFE), END]) + "\n"
    f = root / ".cursorignore"
    old = f.read_text() if f.exists() else ""
    old = re.sub(re.escape(BEGIN) + ".*?" + re.escape(END) + "\n?", "", old, flags=re.S)
    f.write_text(old + block)
    settings = load_settings(root)
    gate = settings.setdefault("chat.tools.edits.autoApprove", {})
    gate.update({g: False for g in SENSITIVE})
    (root / ".vscode").mkdir(exist_ok=True)
    (root / ".vscode" / "settings.json").write_text(json.dumps(settings, indent=2) + "\n")
    print("wrote .cursorignore and .vscode/settings.json")


def cmd_audit(root, strict):
    deny, allow = cursor_patterns(root)
    gate = load_settings(root).get("chat.tools.edits.autoApprove", {})
    gate_globs = [g for g, v in gate.items() if v is False]
    gaps = 0
    for path in walk(root):
        if not is_sensitive(path):
            continue
        cur = matches(path, deny) and not matches(path, allow)
        vsc = matches(path, gate_globs)
        gaps += (not cur) + (not vsc)
        print(f"{path:<28} cursor={'ok ' if cur else 'GAP'} vscode={'ok ' if vsc else 'GAP'}")
    print(f"gaps: {gaps}")
    return 1 if (strict and gaps) else 0


def cmd_scan(root, staged):
    if staged:
        names = subprocess.check_output(
            ["git", "diff", "--cached", "--name-only", "--diff-filter=ACM"], cwd=root, text=True).split()
    else:
        names = list(walk(root))
    hits = 0
    for name in names:
        if name == "secretsguard.py" or name.startswith("tests/"):
            continue
        try:
            text = (root / name).read_text(errors="ignore")
        except OSError:
            continue
        for rule, rx in CONTENT_RULES.items():
            for m in rx.finditer(text):
                line = text.count("\n", 0, m.start()) + 1
                print(f"{name}:{line}: {rule}")
                hits += 1
    print(f"findings: {hits}")
    return 1 if hits else 0


def cmd_hook(root):
    hook = root / ".git" / "hooks" / "pre-commit"
    hook.write_text("#!/bin/sh\nexec python3 secretsguard.py scan --staged\n")
    hook.chmod(0o755)
    print("installed .git/hooks/pre-commit")


def main(argv=None):
    ap = argparse.ArgumentParser(prog="secretsguard")
    ap.add_argument("command", choices=["init", "audit", "scan", "hook"])
    ap.add_argument("--root", default=".")
    ap.add_argument("--strict", action="store_true")
    ap.add_argument("--staged", action="store_true")
    a = ap.parse_args(argv)
    root = Path(a.root).resolve()
    if a.command == "init": cmd_init(root)
    elif a.command == "audit": return cmd_audit(root, a.strict)
    elif a.command == "scan": return cmd_scan(root, a.staged)
    elif a.command == "hook": cmd_hook(root)
    return 0


if __name__ == "__main__":
    sys.exit(main())

The test file:

import unittest
from secretsguard import glob_to_regex, is_sensitive, matches


class GlobTests(unittest.TestCase):
    def test_root_level_env_matches(self):
        self.assertTrue(is_sensitive(".env"))

    def test_nested_env_matches(self):
        self.assertTrue(is_sensitive("services/api/.env.production"))

    def test_example_file_is_safe(self):
        self.assertFalse(is_sensitive(".env.example"))

    def test_pem_anywhere(self):
        self.assertTrue(is_sensitive("infra/certs/server.pem"))

    def test_source_file_not_sensitive(self):
        self.assertFalse(is_sensitive("src/environment.ts"))

    def test_star_does_not_cross_slash(self):
        self.assertFalse(glob_to_regex("*.key").match("a/b.key"))

    def test_matches_any(self):
        self.assertTrue(matches("a/b.key", ["**/*.key"]))


if __name__ == "__main__":
    unittest.main()

The script has four commands. init writes both editor controls. audit reports coverage and, with --strict, fails on gaps. scan checks contents, either across the tree or only for staged files with --staged. hook installs the pre-commit hook. A typical daily flow is init once per repo, hook once per clone, and audit --strict plus scan in CI.

Extend it by adding patterns to SENSITIVE and rules to CONTENT_RULES, then add a test for each new pattern. For more developer-tool walkthroughs like this one, browse the software tutorials section.

Common Pitfalls

These seven mistakes account for most failures I saw while building and testing this project.

  1. Hiding the example file by accident. The pattern **/.env.* also matches .env.example. Add the negation line !**/.env.example, or agents lose the one file that documents your variables.
  2. Using negation inside an excluded directory. Cursor’s docs warn that you cannot re-include a file when a parent directory is excluded with a wildcard. Their example: after public/*, the line !public/index.html works, but !public/assets/style.css does not. Exclude public/assets/* explicitly, then negate the file.
  3. Assuming the ignore file stops the terminal. It does not. The docs place terminal and MCP tools outside its reach. An agent that runs cat .env after you click approve will read it.
  4. Treating the VS Code edit gate as a read block. chat.tools.edits.autoApprove governs edits. It does not hide the file from a read. Keep that distinction in your head when you explain the setup to teammates.
  5. Clobbering a commented settings file. VS Code accepts comments in settings.json and Python’s JSON parser rejects them. The script refuses to write when parsing fails. Do not remove that guard to get past the error.
  6. Relying on .gitignore alone. Cursor honors it, VS Code does not use it for agent controls in anything I found, and neither helps once a secret is already committed. A leaked key needs rotation, not an ignore rule.
  7. Granting approvals at workspace or user scope. VS Code recommends session-level grants. A permanent grant quietly outlives the task that justified it.

Troubleshooting

When something misbehaves, work down this list. Each item gives the symptom, the likely cause and the fix.

  1. ImportError: Start directory is not importable when running tests. The tests folder has no __init__.py. Create an empty one and rerun with -t ..
  2. settings.json has comments or a syntax error on init. Your file uses comments or trailing commas. Add the chat.tools.edits.autoApprove block by hand, copying the JSON from Step 7, then rerun audit.
  3. Audit shows cursor=GAP right after init. The .cursorignore was written to a different folder. Pass --root to point at the repository root, or run the script from that folder.
  4. .env.example shows as a gap or disappears from Cursor. Check that the negation line sits after the **/.env.* line in .cursorignore. Order matters in gitignore syntax, since later patterns override earlier ones.
  5. Cursor still reads an ignored file. Confirm the file is saved in the workspace root you opened, and that the Hierarchical toggle is on if the ignore file sits in a parent folder. Restart the window so Indexing refreshes. Then retest with git check-ignore -v from Step 9.
  6. VS Code applies an edit to .env without asking. A broader auto-approval rule or a session-level grant is winning. Open the Chat permissions menu, reset the grants, and confirm the workspace setting loaded from .vscode/settings.json.
  7. Settings do not apply in VS Code. The folder may be in Restricted Mode, which disables agents entirely, or you edited the wrong settings scope. Trust the workspace if you own it, then reload.
  8. The pre-commit hook never fires. Git only runs hooks marked executable in .git/hooks. Rerun python3 secretsguard.py hook. Also check that you did not commit with --no-verify.
  9. The scanner flags its own source or test files. The script skips secretsguard.py and tests/ by name. If you renamed either, update the skip rule in cmd_scan.
  10. False positives on long config strings. The generic-token rule matches any quoted value of 20 or more characters next to words like token or secret. Tighten the regex for your stack, or move the rule into a separate file you can tune without touching the rest.
  11. git check-ignore prints nothing for a file you expected to match. Git treats a tracked file differently unless you pass --no-index. Include that flag, as in Step 9.

Advanced Tips

Once the basics work, these upgrades close the remaining gaps.

Keep real secrets out of the workspace. This is the strongest control and the only one that survives the terminal and MCP gaps. Load credentials from a secret manager or from environment variables set outside the project folder, and leave only a .env.example in the repo. An agent cannot print a file that does not exist.

Use the hierarchical ignore toggle in monorepos. A single .cursorignore at the root covers the tree, but teams that own sub-packages often want local rules next to the code. With the toggle on, Cursor searches parent directories for ignore files, so package owners can tighten their own scope.

Constrain the shell, not just the files. Turn on VS Code’s agent sandboxing and set the network permission explicitly. The sandbox uses operating-system isolation for shell commands and child processes, and the docs say it can also confine local MCP servers that use the stdio transport on macOS and Linux. Remember its limits: injected credentials, allowed paths and open network access all weaken it.

Isolate the whole session in a container. A dev container gives the agent a filesystem that holds only what you mounted. Our VS Code dev container setup for AI agents shows a layout where secrets arrive as injected environment variables and never sit in the project tree.

Audit cloud runs before you start them. Local ignore files and VS Code settings do not govern cloud services. Before you point a cloud agent or cloud session at a repository, run scan on the full history with a dedicated tool, rotate anything it finds, and check what the remote environment can reach.

Add a rotation step to your incident plan. If the canary test or a scan shows a real key reached an agent, assume the key leaked, rotate it, and check provider logs for use. An ignore rule added afterward protects the future, not the past.

FAQ

Does .cursorignore stop Cursor from reading my .env file?

It blocks the file from Agent, Tab and @ mention references, according to Cursor’s documentation. It does not govern the terminal or MCP tools that Agent uses, and Cursor states that complete protection is not guaranteed. Treat it as one layer.

Does VS Code have a .cursorignore equivalent?

I found no per-file read-block file in the VS Code agent documentation I checked. VS Code relies on workspace-limited file access, approval gates, Workspace Trust and sandboxing. The chat.tools.edits.autoApprove setting forces manual approval for edits to files you list, which is the closest file-level control.

Is a global ignore list better than a project .cursorignore?

Use both. The global list protects every repository you open on your machine, including fresh clones. The project file travels with the repo and protects teammates who never touched your settings.

Can an AI agent still run cat .env in the terminal?

Yes, if you approve the command. Cursor places terminal tools outside the ignore file, and VS Code’s auto-approval rules use best-effort command parsing with known gaps. Keep terminal approvals manual and keep real secrets out of the workspace.

Why did my pre-commit hook not stop a commit?

Check that the hook file is executable and that the commit did not use --no-verify. Hooks are local, so each clone needs its own install. Back the hook with the CI scan so a bypass still fails the pull request.

Do these settings work in cloud agents and cloud sessions?

Do not assume so. The VS Code docs say local approval and sandbox settings do not configure cloud services, and cloud sessions are marked experimental. Cursor cloud agents run in their own environments. Scan and clean the repository before you hand it to either.

Which editor is safer for secrets, Cursor or VS Code?

Neither wins outright. Cursor gives you a direct read block that VS Code lacks, and VS Code documents agent sandboxing for shell commands and offers an edit gate. The safest setup uses the shared pattern list from this tutorial, keeps secrets out of the workspace, and checks both editors with a canary.

Sofia Lindström

Sofia Lindström

Editor-in-Chief

Sofia Lindström is the Editor-in-Chief at Tech Insider, where she leads editorial strategy and oversees coverage across AI, cybersecurity, and enterprise technology. With over a decade in Swedish tech journalism, she previously served as technology editor at Dagens Industri and covered the Nordic startup ecosystem for Breakit. Sofia holds an MSc in Media Technology from KTH Royal Institute of Technology and is a frequent speaker at Web Summit and Slush. She is passionate about making complex technology accessible to business leaders.

View all articles