AI 编程工具越强,开发者越容易犯一个错误:

直接把整个项目交给 AI。

以前我们只是复制一段代码给 ChatGPT,它最多看见一个函数。
现在 Cursor、Kiro、Codex、Claude Code 这类工具更像项目级助手,可以读仓库、理解上下文、生成 patch,甚至帮你跑一段完整任务。

这当然方便。

但问题也更明显:

项目里不是所有东西都适合给 AI 看。

比如:

.env
.env.production
id_rsa
server.key
storage/logs/error.log
database.sqlite
backup.sql
config/database.php
uploads/user_avatar/

这些文件里可能有数据库连接、API Key、用户数据、服务器地址、日志堆栈、支付配置、Cookie、Token,甚至私钥。

你让 AI 读项目之前,如果不先设边界,就很容易把不该共享的内容一起带进去。

所以我现在更建议在项目里准备一个类似 .gitignore 的文件:

.aiignore

它的作用很简单:告诉 AI 编程工具,哪些内容不应该进入上下文。

这篇文章就写一个 Python 脚本,自动扫描项目,生成 .aiignore 和安全检查报告。

它不是完整的安全审计工具,但非常适合作为开发者把项目交给 AI 前的第一道检查。


一、为什么需要 .aiignore

很多开发者已经习惯写 .gitignore

比如:

node_modules/
vendor/
.env
dist/
coverage/

.gitignore 解决的是“哪些文件不提交到 Git”。

AI 编程工具要解决的是另一个问题:

哪些文件不应该被 AI 读取、分析、总结、上传或参与上下文。

这两个范围有重叠,但不完全一样。

比如:

.env.example 通常可以提交到 Git,但如果里面写了真实示例 key,就不一定适合给 AI。
日志文件可能没进 Git,但本地 AI 工具扫描目录时可能看得到。
数据库备份不一定在仓库里,但可能放在项目根目录。
一些内部接口文档可以提交,但可能包含真实域名、内部 Token、测试账号。

所以 .aiignore 的价值是:
在 AI 读项目之前,先给它一条边界线。


二、这个脚本要做什么

我们写一个 make_aiignore.py,功能包括:

  1. 扫描项目目录;
  2. 跳过 node_modulesvendor.git 等无意义目录;
  3. 根据文件名识别高风险文件;
  4. 根据文件内容识别疑似 token、password、数据库连接串、私钥;
  5. 生成 .aiignore
  6. 生成一份 AI_SAFE_SHARE_REPORT.md
  7. 给出一段适合发给 AI 的项目读取边界提示词。

生成后的 .aiignore 可以放在项目根目录。

生成后的 Markdown 报告可以人工看,也可以发给 AI,让它先知道哪些地方不能碰。


三、完整 Python 脚本

新建文件:

touch make_aiignore.py

写入下面代码:

#!/usr/bin/env python3
from __future__ import annotations

import argparse
import fnmatch
import os
import re
from dataclasses import dataclass
from pathlib import Path


DEFAULT_SKIP_DIRS = {
    ".git", ".svn", ".hg",
    "node_modules", "vendor",
    "dist", "build", ".next", ".nuxt",
    ".venv", "venv", "__pycache__",
    ".idea", ".vscode",
    "coverage", "target", "bin", "obj",
    ".pytest_cache", ".mypy_cache",
    "storage/logs", "runtime", "cache", "logs",
}

DEFAULT_SAFE_EXTENSIONS = {
    ".py", ".js", ".jsx", ".ts", ".tsx", ".vue",
    ".php", ".go", ".rs", ".java", ".kt", ".cs",
    ".md", ".yml", ".yaml", ".json", ".toml",
    ".xml", ".sql", ".sh", ".css", ".scss", ".html",
}

SENSITIVE_FILE_PATTERNS = [
    ".env",
    ".env.*",
    "*.pem",
    "*.key",
    "*.p12",
    "*.pfx",
    "*.crt",
    "*.cer",
    "id_rsa",
    "id_ed25519",
    "config/*secret*",
    "config/*password*",
    "*credential*",
    "*credentials*",
    "*token*",
    "*secret*",
    "*private*",
    "*.sqlite",
    "*.db",
    "*.log",
]

SENSITIVE_CONTENT_RULES = [
    (
        "API Key / Token",
        re.compile(
            r"(?i)(api[_-]?key|access[_-]?token|secret[_-]?key|bearer)"
            r"\s*[:=]\s*['\"]?[^'\"\s]+"
        ),
    ),
    (
        "Password",
        re.compile(
            r"(?i)(password|passwd|pwd)\s*[:=]\s*['\"]?[^'\"\s]+"
        ),
    ),
    (
        "Private Key",
        re.compile(r"-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----"),
    ),
    (
        "Database URL",
        re.compile(r"(?i)(mysql|postgres|mongodb|redis)://[^ \n'\"]+"),
    ),
]


@dataclass
class Finding:
    path: str
    reason: str
    evidence: str


def normalize(path: Path) -> str:
    return str(path).replace(os.sep, "/")


def is_in_skip_dir(relative: str) -> bool:
    parts = relative.split("/")

    for index in range(1, len(parts) + 1):
        if "/".join(parts[:index]) in DEFAULT_SKIP_DIRS:
            return True

    return False


def matches_sensitive_name(relative: str) -> str | None:
    name = Path(relative).name

    for pattern in SENSITIVE_FILE_PATTERNS:
        if fnmatch.fnmatch(relative, pattern) or fnmatch.fnmatch(name, pattern):
            return f"文件名命中敏感规则:{pattern}"

    return None


def read_text_head(path: Path, max_bytes: int = 200_000) -> str:
    data = path.read_bytes()[:max_bytes]
    return data.decode("utf-8", errors="replace")


def scan_file_content(path: Path) -> tuple[str, str] | None:
    try:
        text = read_text_head(path)
    except OSError:
        return None

    for title, pattern in SENSITIVE_CONTENT_RULES:
        match = pattern.search(text)

        if match:
            sample = match.group(0)[:120].replace("\n", "\\n")
            return title, sample

    return None


def collect_findings(root: Path) -> list[Finding]:
    findings: list[Finding] = []

    for path in sorted(root.rglob("*")):
        if not path.is_file():
            continue

        relative = normalize(path.relative_to(root))

        if is_in_skip_dir(relative):
            continue

        name_reason = matches_sensitive_name(relative)

        if name_reason:
            findings.append(
                Finding(
                    path=relative,
                    reason=name_reason,
                    evidence="文件名风险",
                )
            )
            continue

        if path.suffix.lower() not in DEFAULT_SAFE_EXTENSIONS:
            continue

        content_result = scan_file_content(path)

        if content_result:
            title, evidence = content_result
            findings.append(
                Finding(
                    path=relative,
                    reason=f"内容疑似包含敏感信息:{title}",
                    evidence=evidence,
                )
            )

    return findings


def build_aiignore(findings: list[Finding]) -> str:
    lines = [
        "# Generated by make_aiignore.py",
        "# 用于限制 AI 编程工具读取项目时应忽略的敏感文件",
        "",
        ".env",
        ".env.*",
        "*.pem",
        "*.key",
        "*.p12",
        "*.pfx",
        "*.log",
        "*.sqlite",
        "*.db",
        "storage/logs/",
        "runtime/",
        "cache/",
        "",
        "# Detected risky files",
    ]

    for item in findings:
        lines.append(item.path)

    return "\n".join(dict.fromkeys(lines)) + "\n"


def build_report(findings: list[Finding]) -> str:
    lines = [
        "# AI 项目共享前安全检查报告",
        "",
        "## 总体结论",
        "",
    ]

    if findings:
        lines.append(f"- 发现 `{len(findings)}` 个建议加入 `.aiignore` 的风险项。")
        lines.append("- 不建议直接把完整项目目录交给 AI,请先检查下方文件。")
    else:
        lines.append("- 未发现明显敏感文件或敏感内容命中。")
        lines.append("- 仍建议人工检查 `.env`、配置文件、日志、上传目录和数据库备份。")

    lines.extend(["", "## 风险文件列表", ""])

    if findings:
        for item in findings:
            lines.extend(
                [
                    f"### `{item.path}`",
                    f"- 原因:{item.reason}",
                    f"- 证据:`{item.evidence}`",
                    "",
                ]
            )
    else:
        lines.append("- 暂无。")

    lines.extend(
        [
            "",
            "## 建议给 AI 的项目读取边界",
            "",
            "```text",
            "请先阅读项目中允许共享的文件,不要读取 .env、日志、数据库备份、证书、私钥、用户上传文件和任何包含 token/password/secret 的配置。",
            "如果你认为需要这些信息,请先说明原因,由我人工提供脱敏版本。",
            "```",
            "",
        ]
    )

    return "\n".join(lines)


def main() -> None:
    parser = argparse.ArgumentParser(
        description="Generate .aiignore and safe sharing report for AI coding tools."
    )

    parser.add_argument(
        "--root",
        default=".",
        help="项目根目录",
    )

    parser.add_argument(
        "--aiignore",
        default=".aiignore",
        help="输出 .aiignore 文件路径",
    )

    parser.add_argument(
        "--report",
        default="AI_SAFE_SHARE_REPORT.md",
        help="输出安全报告路径",
    )

    args = parser.parse_args()

    root = Path(args.root).resolve()

    if not root.exists() or not root.is_dir():
        raise SystemExit(f"Root directory not found: {root}")

    findings = collect_findings(root)

    Path(args.aiignore).write_text(build_aiignore(findings), encoding="utf-8")
    Path(args.report).write_text(build_report(findings), encoding="utf-8")

    print(f"Generated: {args.aiignore}")
    print(f"Generated: {args.report}")
    print(f"Risk findings: {len(findings)}")


if __name__ == "__main__":
    main()

四、运行方式

在项目根目录执行:

python make_aiignore.py --root . --aiignore .aiignore --report AI_SAFE_SHARE_REPORT.md

如果你想扫描某个指定项目:

python make_aiignore.py \
  --root ./my-project \
  --aiignore ./my-project/.aiignore \
  --report ./my-project/AI_SAFE_SHARE_REPORT.md

执行后会生成两个文件:

.aiignore
AI_SAFE_SHARE_REPORT.md

五、用一个示例项目测试

创建一个测试目录:

mkdir aiignore-demo
cd aiignore-demo

mkdir src config
touch src/app.py
touch .env
touch config/database.py

写入一些模拟内容:

echo 'OPENAI_API_KEY=sk-demo-key' > .env
echo 'print("hello")' > src/app.py
echo "DATABASE_URL='mysql://user:pass@localhost/demo'" > config/database.py

运行脚本:

python ../make_aiignore.py \
  --root . \
  --aiignore .aiignore \
  --report AI_SAFE_SHARE_REPORT.md

输出类似:

Generated: .aiignore
Generated: AI_SAFE_SHARE_REPORT.md
Risk findings: 2

生成的 .aiignore 大概会是这样:

# Generated by make_aiignore.py
# 用于限制 AI 编程工具读取项目时应忽略的敏感文件

.env
.env.*
*.pem
*.key
*.p12
*.pfx
*.log
*.sqlite
*.db
storage/logs/
runtime/
cache/

# Detected risky files
config/database.py

生成的 AI_SAFE_SHARE_REPORT.md 会类似这样:

# AI 项目共享前安全检查报告

## 总体结论

- 发现 `2` 个建议加入 `.aiignore` 的风险项。
- 不建议直接把完整项目目录交给 AI,请先检查下方文件。

## 风险文件列表

### `.env`
- 原因:文件名命中敏感规则:.env
- 证据:`文件名风险`

### `config/database.py`
- 原因:内容疑似包含敏感信息:Database URL
- 证据:`mysql://user:pass@localhost/demo`

这说明脚本至少识别到了两个风险:

  1. .env 文件名本身高风险;
  2. config/database.py 里出现了数据库连接串。

六、为什么不能只靠 .gitignore

很多人会觉得:

“我项目里已经有 .gitignore 了,还要 .aiignore 干嘛?”

原因是两者目标不一样。

.gitignore 关注的是:

哪些文件不要提交到 Git。

.aiignore 关注的是:

哪些文件不要进入 AI 上下文。

举几个例子。

1. 本地日志

日志通常不会提交到 Git,但 AI 工具如果能读本地目录,可能会读到。

日志里可能有:

  • 用户邮箱
  • 手机号
  • 请求参数
  • 内部接口
  • 报错堆栈
  • token 片段

这些内容不一定适合交给 AI。

2. 数据库文件

比如:

dev.sqlite
local.db
backup.sql

这些文件可能只是本地开发用,但里面可能有真实测试数据。

3. 配置文件

有些项目会把配置文件提交到 Git,但里面可能仍然有内部域名、默认密码、测试账号。

比如:

config/database.php
application.yml
docker-compose.override.yml

这些文件不一定不能给 AI,但至少应该先人工看一眼。

4. 上传目录

很多 Web 项目有:

public/uploads/
storage/app/

里面可能是用户上传文件、图片、Excel、合同、附件。

这些更不应该直接进入 AI 上下文。


七、可以继续扩展的规则

这个脚本是基础版,你可以根据技术栈继续加规则。

1. 前端项目

可以加:

SENSITIVE_FILE_PATTERNS.extend([
    ".npmrc",
    ".yarnrc",
    "firebase.json",
    "vercel.json",
])

如果 .npmrc 里有私有源 token,不应该直接给 AI。

2. Java / Spring Boot

可以重点扫:

application-prod.yml
application-prod.properties
bootstrap.yml

规则可以加:

SENSITIVE_FILE_PATTERNS.extend([
    "application-prod.yml",
    "application-prod.properties",
    "bootstrap.yml",
])

3. PHP 项目

可以重点扫:

.env
config/database.php
config/app.php
storage/logs/
public/uploads/

如果是 Laravel,.envstorage/logs/ 基本应该默认忽略。

4. Python 项目

可以重点扫:

.env
settings.py
local_settings.py
*.sqlite3

比如 Django 项目里,settings.py 可能包含数据库配置和 SECRET_KEY。

5. Docker 项目

可以重点扫:

docker-compose.override.yml
.env
secrets/

容器编排文件里经常出现数据库账号、Redis 密码、内部服务域名。


八、把 .aiignore 放进团队流程

如果团队里已经开始用 AI 编程工具,不建议每个人各用各的习惯。

可以在项目根目录加:

.aiignore
AI_SAFE_SHARE_REPORT.md

再在 README 里加一段说明:

## AI 工具使用边界

使用 ChatGPT、Claude、Cursor、Kiro 等 AI 工具分析本项目时,请先阅读 `.aiignore`。

禁止直接上传或共享:

- `.env`
- 私钥、证书
- 线上日志
- 数据库备份
- 用户上传文件
- 包含 token、password、secret 的配置

如果你们有 PR 模板,也可以加:

## AI 工具检查

- [ ] 是否使用 AI 辅助修改代码?
- [ ] 是否确认没有把敏感文件交给 AI?
- [ ] 是否更新或检查 `.aiignore`?
- [ ] 是否人工 Review 了 AI 输出内容?

这样 AI 不是“个人偷偷用”,而是进入团队可控流程。


九、把安全边界提示词一起给 AI

除了生成 .aiignore,你还可以把下面这段提示词固定放进 AI 对话开头:

你现在要辅助我分析一个项目。

请遵守这些边界:

1. 不要要求我提供 .env、私钥、证书、数据库备份、线上日志、用户上传文件;
2. 如果你认为需要敏感信息,请说明原因,我会提供脱敏版本;
3. 不要根据缺失的敏感配置自行编造真实值;
4. 不要输出任何看起来像真实 token、password、secret 的内容;
5. 对不确定的信息,请标注“需要人工确认”。

在分析代码前,请先告诉我:
- 你需要哪些非敏感文件;
- 哪些文件不应该提供;
- 你将如何控制分析范围。

这段提示词的作用不是让 AI 更聪明,而是让它更守边界。

AI 参与项目开发时,边界感比“多写几行代码”更重要。


十、工具订阅轻提示

如果你长期使用 ChatGPT Plus、Claude Pro、Grok、Gemini Advanced、Cursor、Kiro 这类 AI 工具,也可以顺手了解 gpt68.com。它是 AI会员充值平台,适合作为 AI 工具订阅充值入口之一。

但对开发者来说,订阅工具只是第一步。

真正让 AI 安全进入项目开发的,是你有没有清楚的上下文边界、敏感文件规则和人工验收流程。

别让 AI 读了不该读的东西,也别让它改了不该改的地方。


十一、代码验证范围

这份脚本已做基础验证:

验证环境:
- Python 3.11
- macOS / Linux 风格路径
- 示例项目包含 .env、src/app.py、config/database.py

验证结果:

- Python 语法检查通过
- 能正常生成 .aiignore
- 能正常生成 AI_SAFE_SHARE_REPORT.md
- 能识别 .env 文件名风险
- 能识别 config/database.py 中的 mysql:// 数据库连接串
- 能跳过 node_modules、vendor、.git 等目录

需要注意:

- 这是关键词和文件名扫描,不是完整安全审计
- 命中风险不代表一定泄露,只代表需要人工检查
- 未命中风险也不代表 100% 安全
- 不同团队需要根据自己的技术栈补充规则
- 真正提供给 AI 前,仍建议人工打开报告检查一遍

十二、总结

AI 编程工具越像项目助手,开发者越要先想清楚:

哪些内容可以给它看?
哪些内容必须忽略?
哪些内容只能脱敏后提供?
哪些地方必须人工确认?

.aiignore 解决的就是这个边界问题。

它不是为了限制 AI,而是为了让 AI 在更安全的范围内发挥作用。

这篇文章里的脚本很简单,但可以放进日常流程:

扫描项目
→ 生成 .aiignore
→ 生成安全报告
→ 人工检查风险项
→ 再把安全上下文交给 AI

AI 可以帮你读代码、改 bug、写测试、整理文档。

但前提是:
你先别把不该给它看的东西,一起塞进去。

更多推荐