Rules / CFG129
CFG129 committed Python bytecode runs instead of its source
Severity: error (an unchecked-hash .pyc in __pycache__ with its source beside it) · warn (an unchecked-hash .pyc with no source yet, bytecode outside __pycache__ with no source)
OWASP: LLM04:2026 – Supply Chain
Agents: claude-code, cline, codex, continue, copilot, cursor, devin, devin-desktop, gemini-cli, goose, grok, junie, kimi-code, opencode, qwen-code, roo-code, vscode, zed
What cfgaudit checks
A hook, task, MCP launch or skill runs a Python script, and that script imports a module beside it. Whatever reads only the .py files reviews the source, but Python may load a committed .pyc instead (#1398, AVE-2026-00075). Since PEP 552 a .pyc header can mark the file as hash-based and unchecked, which tells Python not to look at the source at all.
Measured with CPython 3.12.3 on python main.py, where main.py runs import helper:
.pyc |
helper.py beside it |
Unchecked hash | Checked hash | Timestamp |
|---|---|---|---|---|
__pycache__/helper.cpython-312.pyc |
yes | bytecode runs | source runs | source runs |
__pycache__/helper.cpython-312.pyc |
no | not imported | not imported | not imported |
helper.pyc outside __pycache__ |
yes | source runs | source runs | source runs |
helper.pyc outside __pycache__ |
no | bytecode runs | bytecode runs | bytecode runs |
So CFG129 reports:
- An unchecked-hash
.pycin__pycache__with its source beside the directory →error. - An unchecked-hash
.pycin__pycache__with no source →warn. Python does not import it today, but it would run in place of any source added later, and nobody writes this file by accident. - A
.pycoutside__pycache__with no source beside it →warn, in any invalidation mode. There is no source to review.
Not reported: timestamp and checked-hash files in __pycache__, which Python validates against the source. A timestamp file is what an accidentally committed __pycache__ holds, and after a clone its recorded time no longer matches. Also not reported: a .pyc older than Python 3.7, and a file that is not bytecode.
Only the first eight bytes of the file are read, the magic number and the flags. cfgaudit does not analyse the bytecode, so it cannot tell whether the code differs from the source, and it runs nothing. In a git checkout only tracked files count, so a local __pycache__ is not reported. Without a git index, as in an unpacked download, every .pyc outside node_modules counts.
The cache tag must match the interpreter (cpython-312 for Python 3.12), so an attacker ships one file per version. Each is reported.
Trigger example
A skills/review/scripts/ directory with main.py, a harmless helper.py, and __pycache__/helper.cpython-312.pyc compiled from other code with py_compile's UNCHECKED_HASH mode.
Safe alternative
Do not commit bytecode. Add __pycache__/ and *.pyc to .gitignore, and commit the source of any module the project needs.
References
- PEP 552: Deterministic pycs
- CFG123 (a committed file that stands in for a tool), CFG119 (scripts that auto-run commands start), AVE-2026-00075
Documentation for cfgaudit 1.21.0. Source of this page · Rule implementation