Gemini said yes to the upload, then couldn't load it
Gems are being retired: personal accounts lose them on 2026-12-17, Workspace accounts in
2027. Gemini skills take the same SKILL.md folders as every other AI, so the
obvious move is to upload the skill. On 2026-10-23 that broke two ways. A skill with fonts
was refused outright ("unsupported file type"), and a skill whose upload succeeded failed in
chat ("the skill failed to load") because it held a Markdown file in
references/.
Seven test skills settled it: Gemini loads SKILL.md and images, nothing else.
This tool reshapes any skill to fit.
Point it at a folder
-
python3 tools/agent-skill-to-gemini-skill/agent-skill-to-gemini-skill.py path/to/skill --out dist/geminiwritesdist/gemini/<name>/and<name>.zip. Upload the zip in Gemini under Customize → Skills → Upload, then call it with@name. -
Too many files? Add a
.geminiignoreto the skill (gitignore-style globs,!re-includes) or pass--exclude GLOB. - Exit 1 means a rule is broken; each one is printed with a ✗.
-
Test:
sh tools/agent-skill-to-gemini-skill/agent-skill-to-gemini-skill.test.sh.
What it won't do
-
It doesn't rewrite instructions. "Run
scripts/x.sh" still says so, and Gemini's sandbox has no network. - It doesn't host images. More than 99 need ignoring or a home on the web.
- It doesn't upload. Gemini has no skill API; you upload the zip.
- PDFs are kept as files but weren't tested at load time.
- The rules are a snapshot of Gemini build 20261022.15. A real upload is the final test.
Where it lives
| Integrates with | The Gemini app: its output is uploaded by hand as a skill. |
| Requires |
python3 (standard library only), and git when the skill is
in a repo.
|
| Its state | None. It writes only the --out folder you give it. |
| Safe to delete? | Yes: the output is rebuilt from the skill on every run. |
One skill folder, end to end
convert(src, out, excludes) in agent-skill-to-gemini-skill.py does
it in one pass.
-
List.
source_files()runsgit ls-files -zin the skill; outside a repo it walks the folder.is_junk()drops.DS_Store,Thumbs.db,desktop.ini,._*and__MACOSX/;ignored()applies.geminiignorethen--exclude, last match wins. -
Sort. Extensions in
KEEP(images, PDF) stay files. Anything else that decodes as UTF-8 is inlined; the rest is dropped and named on stderr. -
Inline. Each text file becomes
## FILE: <path>plus a fenced block, the fence one backtick longer than any run inside it. A one-line note above the body tells the model to read those sections where it names a path. -
Check.
NAME_REon the name, then the file count against 100, total bytes against 104857600, and each remaining extension againstALLOWED, the 47 read from Gemini's upload code. -
Write.
<out>/<name>/is replaced, and the zip gets the folder at the top with every entry stamped 2026-01-01, so the same skill always yields the same bytes.