Disclosure: I maintain pyobfus and the pyobfus-mcp server.
Why I built pyobfus
Story starts about six months back. Cardiac imaging research project. Real patent filings in flight, software-copyright applications half-submitted, the kind of work where the lawyers actually read commit messages. The team needed algorithm modules they could hand to outside collaborators as binaries, no readable source. Python, naturally. Every research project is Python now. So: an obfuscator.
PyArmor is the answer when you ask the internet. Fine. Installed PyArmor. Ran it through the test suite, ran it through the build pipeline, shipped the binaries.
Worked great. For about two weeks. Then a collaborator's first production crash came back, and I did what I do with every crash log, which is paste it into Claude Code:
Traceback (most recent call last):
File "dist/algorithms/preprocess.py", line 23, in <module>
AttributeError: 'I0' object has no attribute 'I2'
Reply came back polite. "I don't know what I0 or I2 refer to. Could you share the source?"
Yeah. That was the moment. The protection that kept the algorithm opaque to outsiders had also turned my AI assistant into a polite stranger asking me for source code I'd just spent two weeks deliberately hiding from it. The obfuscator was sitting between the assistant that wrote the code and the assistant that needed to debug it, and the only people it was actually helping were attackers.
I spent something like 40 minutes manually unmapping that trace by hand against the original source, fixed the bug (a missing import, naturally), and then went and surveyed what else was on the market. PyArmor's protection model is one-way by design. Cython compiles to machine code, even further. Neither was solvable without rebuilding the tool from scratch.
So I rebuilt the tool. About a month of evenings vibe-coding with Claude Code itself, organized around a single trade-off: keep the obfuscator's output opaque to outsiders, keep one tiny mapping file readable to me. That's pyobfus 0.4.0, which I shipped on 2026-04-22.
The tools on PyPI were built for a different workflow
Quick history check. PyArmor: 2013. Cython: older still. Oxyry: showed up around 2017. None of them were designed for a world where the thing reading your production logs is a language model. They all assume the same thing: you write code, you obfuscate, you ship, then you read the production logs.
For about a decade that worked fine. Friction on the obfuscator side was friction for attackers (good), and you paid a small ergonomics tax to debug your own production crashes (acceptable, fair trade).
Trade went sideways the year an LLM took over the debug seat. Models can read your trace and your source code side-by-side in the same window (they're disturbingly good at it), but the names have to line up. Trace says I0, source still says UserService, the model has nothing to anchor on. (Polite stranger problem above.)
Used to be a free, invisible cost, paid by humans doing that lookup mentally. Now it's a visible cost, every crash, every customer report, every time.
So the fix can't be "obfuscate less." Obfuscate just as much. Keep one mapping file somewhere only you can reach.
What's in 0.4.0
The release is built around closing that mapping gap. There are four pieces, but really only one matters. I added the other three so that one could be used without ceremony.
Preflight check. Run pyobfus --check src/ and the tool walks your AST looking for things that obfuscation tends to break (eval, exec, dynamic getattr, framework reflection, __all__ exports, __name__ string compares). With --json you get a structured report with an ai_hint field at the bottom that just spells out the next command in plain English. So if it spots FastAPI in your project and finds two high-severity issues, the hint reads "Start with: pyobfus src/ -o dist/ --preset fastapi --dry-run". That hint is the small trick that makes the rest agent-friendly. An MCP-enabled IDE can read the JSON, find the suggested command, and chain it without anyone in the loop typing anything.
Zero-config init. pyobfus --init src/ looks at your imports, decides whether you're on FastAPI / Django / Flask / Pydantic / Click / SQLAlchemy, and drops a pyobfus.yaml next to your code with the matching preset. The YAML has inline comments so when an LLM later reads it back, it has context for why each setting is there.
Save-mapping and unmap. This is the one I wrote the whole release for. When you obfuscate, you pass --save-mapping mapping.json. The dist/ you ship goes to customers. The mapping.json goes wherever you keep secrets (password manager, encrypted vault, private S3, anywhere that isn't inside the artifact and isn't in git). Then a few weeks later when a production trace lands in your inbox, you run:
pyobfus --unmap --trace error.log --mapping mapping.json
and what comes back is that same trace with every identifier restored to what it was before obfuscation. You paste that into Claude Code (or Cursor, or Windsurf) and the AI reads it as if the code had never been obfuscated. The customer's copy is still mangled. Yours isn't.
MCP server. pyobfus-mcp wraps all of the above as a Model Context Protocol server. Once it's installed and your IDE is pointed at it, the assistant can call any of the obfuscation tools from inside a chat turn, without you dropping out to a shell. The five exposed tools are check_obfuscation_risks, generate_pyobfus_config, unmap_stack_trace, list_presets, and explain_preset, and they all return the same {status, payload, ai_hint} JSON envelope.
60-second setup
pip install pyobfus pyobfus-mcp
For Claude Desktop, add to claude_desktop_config.json (macOS path: ~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"pyobfus": {
"command": "pyobfus-mcp"
}
}
}
Restart Claude Desktop, then try a prompt like:
"Check if the src/ folder in my project is safe to obfuscate, and if it's a FastAPI app, generate a pyobfus.yaml for me."
What happens next is that Claude calls check_obfuscation_risks(path="src/"), reads the JSON it gets back, notices suggested_preset: fastapi, then calls generate_pyobfus_config(path="src/", preset_override="fastapi") on its own and hands you the result. Zero shell commands. Cursor, Windsurf, and Zed have slightly different config files; the recipes are in the pyobfus-mcp README.
One nice side effect: the package is live in the official MCP Registry under the name io.github.zhurong2020/pyobfus-mcp, so any MCP client that queries the registry for "python obfuscator" finds it without you doing anything else.
End to end on a toy FastAPI project
Six commands, each a one-liner.
Pre-flight scan:
pyobfus --check src/ --json
The response includes frameworks: [{"name": "FastAPI"}], suggested_preset: "fastapi", and an ai_hint. Zero high-severity findings, we're clear.
Generate config:
pyobfus --init src/ --json
This writes src/pyobfus.yaml with preset: fastapi already selected, framework-aware excludes, and preserve_param_names: true (which you need for FastAPI's Depends() and Pydantic's field-name-to-JSON-key binding).
Obfuscate with a mapping file:
pyobfus src/ -o dist/ -c src/pyobfus.yaml --save-mapping mapping.json --json
The dist/ directory is what you ship. The mapping.json is what you keep. Password manager, encrypted vault, private S3, anywhere that's not in the artifact and not in git.
Weeks later, a customer crash:
File "dist/routers/users.py", line 23
AttributeError: 'I0' object has no attribute 'I2'
Reverse it:
pyobfus --unmap --trace error.log --mapping mapping.json
Output:
File "dist/routers/users.py", line 23
AttributeError: 'UserService' object has no attribute 'get_profile'
Line numbers still point at the obfuscated file (known limitation, sitting on the v0.5 list), but every identifier is the original. Paste that into Claude Code and you're effectively back to debugging your own source. The AI suggests a fix, you apply it, you ship the patch, you move on with your evening.
The customer-facing copy is still as mangled as it was the day you shipped it. They still see I0 and I2. The only thing that links the two halves of the world is mapping.json, which lives on your machine and nowhere else.
Threat model + what you actually get
I should be honest about what pyobfus is. It's name-mangling plus optional string encryption. It's not bytecode-level encryption, it's not VM-style virtualization (which is the lane PyArmor 9.2 went down in late 2025), and a sufficiently motivated reverse engineer with enough hours on their hands can take most of it apart. The community tier in particular is friction, not a wall. If you're worried about nation-state-grade adversaries, this isn't your tool, and frankly Python probably isn't your language.
What pyobfus does buy you, against the threat model most of us actually have, is roughly four things. Casual scanning of your dist/ directory for class names, API endpoints, or business-logic strings turns up mangled noise. The Pro tier's string encryption hides literal secrets from a strings-style inspection pass. Pro's control-flow flattening makes static analysis genuinely painful. And, the reason you're reading this post: AI-assisted debugging keeps working on production traces, as long as you've kept mapping.json somewhere your AI can reach but your customers can't.
The other obfuscators on PyPI mostly force you to pick between protection and debuggability. pyobfus is my attempt at a third option, where the only thing standing between the two is a single small file you control.
Try it
pip install pyobfus pyobfus-mcp
pyobfus --check your-project/ --json
- Source: https://github.com/zhurong2020/pyobfus
- MCP server: https://github.com/zhurong2020/pyobfus/tree/main/pyobfus_mcp
- Drop-in AI integration templates (CLAUDE.md, .cursorrules, AGENTS.md, etc.): https://github.com/zhurong2020/pyobfus/tree/main/templates/ai-integration
- Full JSON schemas + CLI reference for AI agents: https://github.com/zhurong2020/pyobfus/blob/main/llms-full.txt
v0.5 is in planning. The headline items are layered protection (so you can pick per-module what your AI is allowed to see), a VS Code extension, and the long-overdue dropping of Python 3.8 (EOL was 2024-10 and our CI matrix has been carrying that weight for over a year now). If there's a specific pain you'd like prioritized, GitHub issues are the place.
One last thing. If you do ship with pyobfus, please put mapping.json somewhere safe. It's a small boring JSON file, and six months from now when a customer pings you about a crash, it's the only thing standing between you and 40 minutes of doing what I did manually that first night.


Top comments (0)