DEV Community

Mārtiņš Veiss
Mārtiņš Veiss

Posted on

The one you reported is gone. This one is still up.

On 2026-08-17 I reported Leutenegger/book-to-skill to GitHub Trust & Safety: TLS
verification disabled at import, a beacon to a Cloudflare Worker, eight wallet-extension
IDs enumerated, Ledger data archived and uploaded, and a Windows EXE launched via
PowerShell with ExecutionPolicy Bypass. It was impersonating
book-to-skill — a genuinely useful,
26,868-star project (PDFs into Claude Skills) — riding on its sudden popularity to get
installs. GitHub actioned it on 2026-08-23. The repository and the entire account now
return 404.

That should be the end of it. It isn't. On 2026-08-29, diffing every repository sharing
the project's name against the real upstream tree turned up a second operator, still live
at the time of writing.

What is in the archive

Both of the impersonator's repositories ship the same 487,256-byte zip. One path is
frond/book_to_skill_3.8.zip, the other docs/v1.7.zip — byte-identical, three members,
matching timestamps.

  • Application.cmd — 24 bytes. Its entire contents: start utils.exe cert.txt. One file runs the other.
  • utils.exe — 759,808 bytes, a PE executable. Not malicious on its own: the embedded strings identify an unmodified LuaJIT 2.1, built from the Defold project's pipeline. It's here purely to execute the payload.
  • cert.txt — 183,728 bytes. Not a certificate. Obfuscated Lua source, opening local Tq=function(i)local k,Z=i[#i],""….

The technique is living off a clean binary: ship a legitimate interpreter that no scanner
objects to, keep the actual logic in a data file with a reassuring extension, and let a
24-byte batch file put them together. A file named cert.txt that is executable Lua,
sitting next to an interpreter, inside a repository named after a widely-installed
developer tool, is not an accident.

SHA-256, for anyone who wants to pivot on them in VirusTotal or MalwareBazaar (GitHub
itself can't be searched by hash — more on that below):

Application.cmd  2fd3cb8aa174b29406cd39f62369603891840e90bb65cd8aaf4fb3e32a28f166
cert.txt         9303b32a3fdf241d38aaaaff46ef9fdb0800b678a4ea2be4bf35d475610eb152
utils.exe        e227d47facf0e2f5b9e5c54a23a86b31272696b64bbbb9932a31fe06973677bd
Enter fullscreen mode Exit fullscreen mode

Two doors, one payload

  • 1 repositoryorestedevoted590/book-to-skill, created 30 July. No source code. A one-line README and the archive.
  • 1 Pages siteorestedevoted590.github.io, a published site with real marketing copy, a Download badge, and Windows-only system requirements.
  • 0 inbound links — no stars, no forks, no followers, not in GitHub's code index. Nothing points at either repo or the site.
  • 90 other name-copies checked — every exact-name repository diffed against the upstream tree. One other shipped an archive at all; it turned out to be markdown and YAML, and benign.

The Pages site is the more dangerous of the two. It reads as a legitimate product page —
"Turn PDF books into usable skills," "System requirements: Windows 10 or Windows
11,"
"You must have Claude Code installed" — and it's served from github.io, so it
borrows the platform's own credibility. Nobody has to browse a repository or read a diff
to get caught by it. They only have to click Download.

Four ways of searching that don't work

Worth recording, so nobody burns an afternoon rediscovering the same dead ends:

  • Search by hash. GitHub doesn't expose a searchable content hash — no SHA-1 blob query field, and SHA-256 of file content isn't stored anywhere searchable.
  • Search inside the archive. Zip contents are never indexed. The launcher string start utils.exe cert.txt returns nothing, because it only exists inside a zip.
  • Search the repository's own text. A control query against the malicious Pages site returns 0 results, while the same query shape against a large repository returns 32. The account is too new and too small to be in the code index at all.
  • Search for the payload's shape. The obfuscator randomizes variable names per sample, and code search tokenizes on words, so local k,Z=i[#i] matches nothing. The two Application.cmd/cert.txt hits that do surface are a Mac packaging script and someone's unrelated notes.

What actually worked is unglamorous: notice a name-squat, list every repository sharing
the name, diff each against the upstream tree, and open whatever ships that the original
doesn't.

Not the same operator, as far as the evidence goes

Same impersonation target, same delivery class the first security notice describes — but
this payload is obfuscated and carries no plaintext URLs, so neither Cloudflare Worker
endpoint from the first report appears in it. A link between the two is neither
established nor excluded here, and it shouldn't be implied either way without evidence.


Why this is on a blog about a self-hosted agent platform rather than a security
newsletter: this is a supply-chain problem aimed directly at people building with AI
coding tools. The impersonation target here converts PDFs into Claude Skills — the exact
kind of small, popular, quickly-forked utility that anyone assembling an agent's toolkit
downloads without a second look. The name is the entire distribution strategy. If you
install skills, MCP servers, or agent tools from GitHub by name rather than by verified
publisher, this is what the failure mode looks like from the inside.

I write this from building AutoBot-AI, a
self-hosted agent platform — the same category of project where "check what you're
installing before you run it" is the whole point, not an afterthought.

Static analysis only. Nothing was executed and no interpreter was invoked on the
payload; the second archive was extracted to a temporary directory solely to compute the
hashes above, then deleted. Impersonation target:
github.com/virgiliojr94/book-to-skill
(26,868 stars). Prior case: Leutenegger/book-to-skill, reported 2026-08-17, actioned
2026-08-23.

Top comments (2)

Collapse
 
raknaos profile image
Raknaos

The four dead ends are the most useful part of this. Search by hash, zip contents, the repo's own text, the payload's shape - each looks like it should work until you try it, and they all fail for the same reason: the index only knows what a page contains, not what an archive carries. "An empty result is not evidence of absence" is worth pinning above the workflow.

The line that will stick with me is that nobody has to read a diff to get caught, only click Download on a github.io page. A Pages site borrows the platform's credibility in a way a zero-star repo never does. Did the deployment history say anything about what pushed it, or is that equally invisible from outside?

Collapse
 
mrveiss profile image
Mārtiņš Veiss

Honestly, i forgot to look for that, my target was to report so it gets taken down.