OWNED TOOL · PYTHON SCRIPT

Gems are gone.
Your skill
still loads.

The Gemini app loads only SKILL.md and images. This folds every other text file into SKILL.md, checks Gemini's upload rules, and zips it.

One command, any skill folder, nothing to install beyond python3.

agent-skill-to-gemini-skill.py my-skill → dist/gemini/my-skill.zip
ONE SKILLWHAT HAPPENS
1references/, scripts, CSS: inlined
2Images kept as files
3Fonts, wasm dropped, with a warning
✓≤ 100 files, 47 types, zip out
INSTALL

the full setup, run in a clone of RascalTwo/ai-setup

bun install.ts

needs python3

↓ scroll

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

What it won't do

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.

git ls-files minus junk + ignore image / text / binary keep / inline / drop rules name · 100 · 100 MiB · 47 folder + zip fixed timestamps any ✗ → still written, exit 1
The output is written even when a rule fails, so you can see what Gemini would get.
  1. List. source_files() runs git ls-files -z in the skill; outside a repo it walks the folder. is_junk() drops .DS_Store, Thumbs.db, desktop.ini, ._* and __MACOSX/; ignored() applies .geminiignore then --exclude, last match wins.
  2. 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.
  3. 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.
  4. Check. NAME_RE on the name, then the file count against 100, total bytes against 104857600, and each remaining extension against ALLOWED, the 47 read from Gemini's upload code.
  5. 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.