只给 AI 一个 GitHub 仓库地址,它能安全开源本地项目吗?我做了一个 Codex Skill
很多人已经习惯让 AI 写功能、修 bug、补测试。
但代码写完以后,真正准备开源时,仍然有一串容易出问题的工作:
- 哪些文件应该上传,哪些缓存、日志和本地环境不应该上传?
- .env、私钥、Token 或带账号密码的 URL 有没有混进项目?
- README、LICENSE、运行命令和测试说明是否齐全?
- 目标仓库到底是不是用户刚创建的那个空仓库?
- 本地已经有 origin 时,能不能直接改 URL?
- 远端不是空的,应该合并、更新,还是立即停止?
- git push 返回以后,怎样确认远端分支真的指向本地这次提交?
如果把这些问题全部压缩成一句"帮我上传到 GitHub",AI 当然可以很快执行 git add . && git push。问题是,它也可能把不该上传的东西一起推上去,或者把本地项目推到错误的远端。
所以我没有做一个"更快执行 push"的脚本,而是做了一个会先审计、会拒绝、会留下证据的 Codex Skill:
github-safe-publish
开源地址:https://github.com/wangzifan396-wzf/skills
首个公开提交:https://github.com/wangzifan396-wzf/skills/commit/71c6a9c501b406c31d42413f501afb091ba1baff
它解决的是一个很窄、但非常实际的问题:
用户先在 GitHub 创建一个空仓库,然后把本地项目和仓库地址交给 AI;AI 负责检查发布边界、准备开源材料,并在明确授权后使用普通 Git 操作提交、推送和校验。
为什么做成 Skill,而不是一段超长脚本
项目发布同时包含两类工作。
第一类需要判断:
- 这是源码、文档、演示素材,还是本地生成物?
- dist 是应该排除的构建缓存,还是 GitHub Pages 真正需要的发布产物?
- 一个 60 MiB 视频应该保留在 Git、转成 Release 附件,还是使用 Git LFS?
- 缺少 LICENSE 时应该选择哪一种许可证?
- 非空远端是不是这个项目之前已经发布过的同一段历史?
这些问题不能靠固定扩展名完全决定。比如"所有 build/ 都不上传"看起来省事,却可能直接删掉某些项目真正要交付的静态产物。
第二类工作适合确定性脚本:
- 枚举 Git 已跟踪、未跟踪和已忽略路径;
- 检测常见密钥形态和 GitHub 单文件上限;
- 解析 owner/repository;
- 读取远端 HEAD 和分支;
- 执行 git diff --cached --check;
- 普通提交、普通推送;
- 再次读取远端 ref,并与本地 HEAD 比较。
因此这个项目采用两层结构:Skill 负责判断、授权和编排,Python 脚本负责重复且容易验证的动作。脚本全部只使用 Python 标准库和系统 Git,没有再引入一个新的依赖栈。
我把发布拆成六步

图 1:外部写入发生在扫描、准备、远端检查和 dry-run 之后。结束条件不是"push 命令没有报错",而是远端目标分支 SHA 与本地 HEAD 完全一致。
整个工作流分成六步。
第一步:确认范围与授权
Skill 先确定两个对象:本地项目根目录,以及目标 GitHub 仓库。
"帮我看看能不能发布"只代表只读检查;"把这个项目上传到这个仓库"才代表可以在这个范围内提交和推送。创建 GitHub 仓库、创建 Release、开启 Pages、发 Issue 和发 CSDN 都不自动包含在这份授权里。
第二步:扫描上传边界
扫描器会输出两份材料:
audit.json # 完整机器可读清单
audit.md # 方便人工复核的报告
JSON 中会列出:
- 每个候选文件的相对路径、字节数、是否已跟踪、是否为符号链接;
- Git 返回的已忽略路径;
- 项目类型、常见入口和 package.json scripts;
- README、LICENSE、.gitignore、运行与测试入口是否存在;
- blocker、warning 和建议补充的忽略规则;
- 当前分支、工作树状态和远端信息。
直接运行扫描器的命令是:
python github-safe-publish/scripts/inspect_project.py D:\path\to\project `
--json-out audit.json `
--report-out audit.md
报告默认不会把审计文件自己塞回待发布项目。它们是发布证据,不一定是项目源码的一部分。
第三步:补齐忽略规则和开源文档
Skill 不会看到 node_modules 就直接删除它,而是先生成保守的 .gitignore 建议:
python github-safe-publish/scripts/prepare_gitignore.py D:\path\to\project
上面只预览。确认以后才增加 --write。
默认关注的内容包括:
- node_modules/、.venv/、__pycache__/;
- pytest、mypy、Ruff 等工具缓存;
- .log、.tmp、编辑器 swap 文件;
- .env 和环境专用配置,同时保留 .env.example、.env.sample。
它故意不默认排除 dist/、build/、图片、视频和生成文档,因为这些路径在不同项目中可能具有完全不同的交付语义。
README 和 LICENSE 也不应该机械生成。Skill 会检查它们是否存在,但 README 里的功能、运行命令和测试结果必须来自真实项目;没有许可证意图时,也不能默认替维护者选择 MIT、Apache-2.0 或 GPL。
第四步:只读检查 GitHub 远端
远端检查器只接受标准的 GitHub HTTPS 或 SSH 仓库地址:
https://github.com/OWNER/REPOSITORY
https://github.com/OWNER/REPOSITORY.git
git@github.com:OWNER/REPOSITORY.git
它拒绝带查询参数、/tree/main 等非仓库路径,以及把账号或 Token 嵌进 HTTPS URL 的写法。
执行:
python github-safe-publish/scripts/inspect_remote.py `
https://github.com/OWNER/REPOSITORY `
--json-out remote.json
一个真正的空仓库会得到类似结果:
{
"accessible": true,
"empty": true,
"defaultBranch": null,
"heads": []
}
第五步:默认只做 dry-run
发布器即使拿到了完整参数,默认仍然不会修改任何状态:
python github-safe-publish/scripts/publish_project.py `
--project D:\path\to\project `
--remote https://github.com/OWNER/REPOSITORY `
--confirm-repository OWNER/REPOSITORY `
--branch main `
--commit-message "feat: publish project" `
--receipt publish-plan.json
这里专门要求再写一次 --confirm-repository OWNER/REPOSITORY。它不是为了增加仪式感,而是防止 URL 指向一个名字相近、但不是用户真正目标的仓库。
dry-run 会复述:
- 项目根目录;
- 解析后的仓库所有者和名称;
- 目标分支与远端名;
- 候选文件数和体积;
- 阻断与警告;
- 将要执行的 Git 操作。
但它不会初始化 Git、暂存文件、创建提交、增加远端或推送。
第六步:普通推送,并校验远端 SHA
只有用户明确要求发布,而且 dry-run 已经复核,才在同一条命令后增加:
--execute
发布器会:
- 必要时初始化 Git;
- 暂存已跟踪变更、删除项和扫描器确认的候选文件;
- 执行 git diff --cached --check;
- 有变化时创建普通提交;
- 在不冲突时增加目标远端;
- 使用普通 git push --set-upstream,不带任何 force 参数;
- 重新读取 refs/heads/main;
- 将远端 SHA 与本地 HEAD 逐字比较。
最后还可以独立验证:
python github-safe-publish/scripts/verify_publish.py `
--project D:\path\to\project `
--remote https://github.com/OWNER/REPOSITORY `
--branch main
自动上传最重要的是知道什么时候停

图 2:安全门覆盖本地内容、远端历史、Git 补丁和发布后结果。任何一项阻断都比"先推上去再说"更便宜。
这个 Skill 把下面六类情况设成了明确的停止点。
1. 疑似密钥、私钥和凭据文件
扫描器会关注:
- .env、私钥和 keystore 类文件;
- GitHub github_pat_、ghp_ 等令牌形态;
- AWS Access Key、Slack Token、OpenAI Key;
- password、client_secret、api_key、access_token 等疑似非占位赋值。
报告只给出类别、路径和行号,不会把匹配值复制到 JSON、终端或文章里。
这里还有一个容易忽视的区别:如果秘密文件已经被 Git 跟踪,后来只把它加进 .gitignore 并不能解决问题。它仍然需要从索引或历史中移除,已经可能暴露的凭据还应该旋转。V1 扫描当前发布边界,不替代 GitHub Secret Scanning,也不自动清洗历史。
2. GitHub 单文件上限
文件达到 50 MiB 会警告,超过 GitHub 普通 Git 的 100 MiB 单文件限制会阻断。
警告的意义不是"一律删除大文件",而是让维护者选择:
- 这是不是不该提交的本地生成物?
- 是否更适合作为 Release 附件?
- 是否真的要使用 Git LFS?
3. 非空或无关远端
首次发布默认只面向空仓库。
远端只要已经有分支,就会阻断。对已经由同一本地历史发布过的项目,可以显式增加 --allow-existing,但仍要满足:
- 目标远端已经绑定并经过检查;
- 远端目标分支存在;
- 远端分支是本地 HEAD 的祖先;
- 最终只能执行 fast-forward 普通推送。
无关历史不会自动使用 --allow-unrelated-histories 合并,更不会 force push 覆盖。
4. 远端名已经指向别处
如果本地已有 origin,但它不是用户给出的仓库,发布器不会静默执行 git remote set-url。
这时应该保留原远端,确认用户是不是选错项目,或者在明确情况下选择一个新的远端名。自动改 URL 虽然方便,却也是把正确代码推到错误仓库的高风险来源。
5. 暂存补丁本身不合格
这条安全门在第一次真实发布时就发挥了作用。
最初执行发布,脚本在提交前返回:
{
"status": "failed",
"error": ".gitignore:13: new blank line at EOF. ..."
}
原因是部分新文件末尾多了一行空白。此时文件已经在本地暂存,但没有创建提交,也没有推送远端。
修正 EOF 空行、重新运行 10 个测试和官方 Skill 校验后,第二次发布才继续。这不是一个"差点失败"的插曲,而是我希望这个工具具备的行为:格式问题应该在本地提交前暴露,而不是为了自动化完成率被忽略。
6. 本地与远端 SHA 不一致
git push 进程退出码为 0,只说明 Git 命令认为推送完成。Skill 仍然会查询远端的精确分支 ref。
这次真实回执是:
{
"status": "published",
"branch": "main",
"commitCreated": true,
"localHead": "71c6a9c501b406c31d42413f501afb091ba1baff",
"remoteHead": "71c6a9c501b406c31d42413f501afb091ba1baff",
"verified": true,
"candidateFiles": 16,
"ignoredPathEntries": 2
}
随后独立验证器再次得到 headMatches: true、worktreeClean: true。
Skill 的目录里有什么
仓库当前结构如下:
skills/
├── README.md
├── LICENSE
├── tests/
│ └── test_scripts.py
└── github-safe-publish/
├── SKILL.md
├── agents/
│ └── openai.yaml
├── scripts/
│ ├── inspect_project.py
│ ├── inspect_remote.py
│ ├── prepare_gitignore.py
│ ├── publish_lib.py
│ ├── publish_project.py
│ └── verify_publish.py
└── references/
├── safety-policy.md
├── readiness-checklist.md
└── troubleshooting.md
SKILL.md 保留核心工作流和资源导航。更细的授权、凭据、远端历史规则放在 safety-policy.md;README、LICENSE、运行和测试说明放在 readiness checklist;认证失败、远端非空、大文件和推送拒绝的处理则按需读取 troubleshooting。
这种分层可以避免每次调用 Skill 时把所有细节一次性塞进上下文,同时让真正执行外部写入前必须读取安全策略。
怎样安装和使用
Windows PowerShell
git clone https://github.com/wangzifan396-wzf/skills.git
Copy-Item -Recurse .\skills\github-safe-publish `
"$HOME\.codex\skills\github-safe-publish"
macOS / Linux
git clone https://github.com/wangzifan396-wzf/skills.git
cp -R skills/github-safe-publish ~/.codex/skills/github-safe-publish
重启 Codex,让它重新发现 Skill。
然后先在 GitHub 创建一个真正的空仓库。首次使用建议不要勾选自动创建 README、LICENSE 或 .gitignore,因为这些选项会让远端已经拥有首个提交,V1 会按非空远端处理。
在本地项目目录中可以直接说:
使用 $github-safe-publish,检查当前项目,并发布到
https://github.com/OWNER/REPOSITORY 。
如果只想先看结果,不发布:
使用 $github-safe-publish,只审计当前项目和这个 GitHub 仓库,
生成上传清单、排除清单与修复建议,不要提交或推送。
除了 Python 3.10+ 和 Git,脚本没有第三方运行依赖。GitHub 身份认证使用系统已有的 Git Credential Manager、SSH Agent 或其他正常 Git 认证环境,不需要把 Token 粘贴到对话中。
用它审计两个真实项目

图 3:Skill 先扫描自己,再扫描 mini-browser-games。候选表示进入复核边界,不代表所有内容都会无条件上传;warning 也不等于失败。
为了避免这个 Skill 只对一个临时样例有效,我先让它扫描自身,再扫描了我的 100 款浏览器游戏开源仓库。
结果如下:
| 指标 | github-safe-publish 仓库 | mini-browser-games |
|---|---|---|
| 候选文件 | 16 | 185 |
| 候选体积 | 77.2 KiB | 73.4 MiB |
| 忽略路径项 | 2 | 24 |
| 阻断 | 0 | 0 |
| 警告 | 0 | 2 |
游戏仓库的两个警告分别是:
- 根目录有 100 个独立 HTML 游戏,没有单一的传统 index.html 入口;
- 带字幕的宣传视频约 63.2 MiB,需要人工确认是否继续放在普通 Git 仓库中。
这两个警告都没有被"智能修复"。第一个是仓库本身的产品形态,不是代码错误;第二个涉及演示材料的发布策略,也不应该由一个通用脚本替维护者决定。
这正是清单比"自动排除"更重要的地方:工具负责让差异可见,人负责决定项目真正的边界。
我为脚本写了哪些测试
当前仓库有 10 个标准库 unittest 用例,覆盖:
- HTTPS、.git 后缀、scp 风格 SSH 和 ssh:// URL 归一化;
- 拒绝嵌入 HTTPS 凭据的 URL;
- 拒绝 /tree/main 这类非仓库路径;
- --confirm-repository 必须与目标一致;
- 静态网页项目入口识别;
- node_modules 和 Git ignore 行为;
- 真实 .env 阻断、.env.example 保留;
- 疑似密钥被发现,但原值不会出现在 manifest;
- 超过大小阈值时阻断;
- .gitignore 写入顺序正确且重复执行不重复追加。
执行:
python -m unittest discover -s tests -v
结果是 10/10 通过。Skill 本体还使用官方 quick_validate.py 检查 YAML frontmatter、名称与目录结构,结果为 Skill is valid!。
真实远端测试也覆盖了三种状态:
- 首次空仓库:允许发布并完成 SHA 校验;
- 已发布的非空仓库:默认返回 blocker,dry-run 退出码为 2;
- 同一历史后续更新:只有显式 --allow-existing 时 dry-run 才进入 ready,并仍要求 fast-forward。
V1 故意没有包办一切

图 4:复杂场景不会被包装成"自动化失败",而是转入人工审查或专用工作流。能明确保证安全的部分才进入 V1 自动执行路径。
github-safe-publish 当前没有尝试成为完整的 GitHub 运维平台。
它不会自动:
- 创建 GitHub 仓库;
- 修改仓库 description、topics 或可见性;
- 创建 Release、Tag 或 GitHub Pages;
- 发 Issue、Pull Request、消息或 CSDN;
- 重写 Git 历史、清理已经提交过的秘密;
- 配置 Git LFS、子模块或组织 SSO;
- 把 monorepo 的一个子目录强行拆成独立仓库;
- 覆盖带有无关历史的远端。
这些能力以后可以继续增加,但每一种都有不同的权限和失败语义。把它们全部塞进第一版,只会让一句"上传项目"获得过大的外部操作范围。
V1 的边界更像一句承诺:
对用户已经创建的空仓库,完成可审计的首次发布;对同一历史完成显式、可验证的 fast-forward 更新;其他场景先停下来解释。
这次做 Skill 给我的最大启发
以前我会把"自动化"理解为尽量减少步骤。做完这个 Skill 后,我更愿意把它理解成:
把重复动作交给脚本,把关键判断暴露出来,把外部写入推迟到证据充分以后。
一个真正安全的上传工具,不应该以"成功执行了多少次 push"为唯一指标。它还应该回答:
- 它有没有把秘密值输出到日志?
- 它能不能解释为什么某个文件被排除?
- 它遇到非空远端会不会自作主张?
- 它能不能在提交前停下来?
- 它最后能不能证明远端就是本地这个提交?
这次首发时,安全门真的因为 EOF 空白行停止,反而让我更确认这套设计是有意义的。自动化不是永远向前执行;可靠的自动化必须拥有明确、可解释的刹车。
本文的 5 张图片也不是手工拼接的不可复现成品。图中数字来自 docs/images/github-safe-publish/article-data.json,Pillow 脚本读取这份 JSON 后生成 1920×1080 封面和 4 张 1600×900 正文图:
cd promo-video
npm.cmd run article:github-safe-publish:images
以后 Skill 的文件数、测试数或真实审计结果变化时,只需要先更新结构化数据,再重新生成图片,就不必在多张图里手工修改同一个数字。
项目已经使用 MIT License 开源:GitHub - wangzifan396-wzf/skills · GitHub
如果你也经常让 Coding Agent 完成项目,可以直接安装后拿一个新建的空仓库试用。也欢迎检查脚本、补充 Secret 规则、提交 Issue,或者用它审计自己的开源发布边界。
更多推荐



所有评论(0)