This guide covers debugging the Power Tools add-in from VS Code and from
Zed. Both attach a Python debugger (debugpy) to the running Fusion process;
they differ only in who starts the debug server.
For general setup, the repository layout, and tooling, see the Developer Guide.
- The two debugging models
- Prerequisites
- Pointing the config at a build (
update_debug_path.py) - Enabling the attach-debug server
- Debugging in VS Code
- Debugging in Zed
- When Zed cannot fetch debugpy (offline or proxy)
- Port map
- The four setup traps
- Verification checklist
- Disabling debugging (ship mode)
- After a Fusion update
- When Fusion crashes: reading a CER report
- Reference
Fusion's Debug button in Scripts and Add-Ins is hardcoded to launch VS Code,
and the ms-python extension injects debugpy for you. To use any other DAP
client (Zed), you invert the model: the add-in starts a debugpy server
in-process and the editor attaches.
| VS Code | Zed | |
|---|---|---|
| How the server starts | Fusion's Debug button injects debugpy |
The add-in starts an in-process debugpy server when the .debug marker is present |
| How you launch the add-in | Debug button | Run button (not Debug) |
| Editor role | Attaches automatically | You attach manually (F4) |
| Port | 9000 (.vscode/launch.json) |
5678 (.zed/debug.json, config.DEBUGGER_PORT) |
| Config file | .vscode/launch.json (auto-generated by Fusion) |
.zed/debug.json (this repo) |
Both config folders (.vscode/, .zed/) are git-ignored — they contain
machine-specific absolute paths and the volatile Fusion webdeploy hash, so they
are regenerated per machine.
The in-process server needs debugpy installed into Fusion's bundled
Python (not your .venv). It is a one-time step, repeated after each Fusion
auto-update (the update rotates the webdeploy hash).
The helper scripts in schneik80/Zed_Debug
automate this:
~/Source/Zed_Debug/scripts/setup-fusion-debug.sh /Users/schneik/Source/PowerToolsThe script locates the current Fusion Python, bootstraps pip via ensurepip
(Fusion ships without pip — trap #1), installs debugpy
into Fusion's --user site (which survives Fusion upgrades), and rewrites the
editor config's webdeploy paths.
Channel caveat — the checkout on
ADSKMVG91G2F5W(MacBook Air M4, macOS 26.5.1). Both Fusion devices (ADSKMVG91G2F5Wandg16win.local) have production and pre-production installed side by side — that machine carries five webdeploy trees (production,pre-production,develop,feature--1fx-globalnav,meta). So the debugger must be pointed at the build you are actually running; attaching to the wrong tree looks like breakpoints that never trip.This checkout is configured against a pre-production build (see the paths in
.zed/settings.jsonand.env), whilesetup-fusion-debug.shscanswebdeploy/**production**. Run the stock script and it silently repoints the config at production. The fix isupdate_debug_path.py, which takes the channel as an argument — run it after the setup script to put the paths back, or instead of it wheneverdebugpyis already installed.This is a property of the checkout, not the device — the same choice exists on
g16win.local, under%LOCALAPPDATA%\Autodesk\webdeploy\.On
ADSKMVG91G2F5Wdebugpyis already installed (~/Library/Python/3.14/lib/python/site-packages/debugpy), so no install is needed — only re-run the step after a Fusion update if imports or the listener stop working. That location is the user site for Python 3.14, and every channel checked bundles Python 3.14, so the one install serves production and pre-production alike; it needs redoing only if a Fusion update bumps the bundled Python minor version. Ong16win.local, verify before assuming.The
PYTHONPATHshape is easy to get wrong. The API packages live at<channel>/<hash>/Autodesk Fusion.app/Contents/Api/Python/packages— note theAutodesk Fusion.app/Contents/segment. Check the path in.envstill resolves after any Fusion update; a rotated hash leaves it silently pointing at nothing:ls -d "$(grep -o '/Users/.*packages' .env)" || echo "STALE — re-run setup"A given hash is not unique to a channel —
8d5cf31c…currently appears under bothproduction/andpre-production/— so the hash alone does not tell you which build you are attached to.
Verify at any time:
"<Fusion Python>" -c "import debugpy; print(debugpy.__version__)"Two config values are absolute paths into the webdeploy tree, and both rotate out from under you on every Fusion update:
| File | Key | Points at |
|---|---|---|
.env |
PYTHONPATH |
…/<build>/Autodesk Fusion.app/Contents/Api/Python/packages |
.zed/settings.json |
lsp.pyright.settings.python.pythonPath |
…/<build>/Autodesk Fusion.app/Contents/Frameworks/Python.framework/Versions/Current/bin/python |
Rather than hand-editing them, select a channel and let the script find the newest build that is actually complete:
python3 tools/debug/update_debug_path.py --list # what is available
python3 tools/debug/update_debug_path.py pre-production # repoint both configs
python3 tools/debug/update_debug_path.py # prompt for a channel
python3 tools/debug/update_debug_path.py develop --dry-runpre-production
-> 5c7e4bae1a38 Fusion 2705.1.4 deployed 2026-08-21 19:26
production
-> 5b508d94493e Fusion 2704.1.53 deployed 2026-08-05 09:50
It writes only those two keys — extraPaths, the Debugpy adapter path and
anything else in .zed/settings.json are preserved — and skips
.zed/settings.json entirely if the file is absent. Both files are
git-ignored, so this is per-device state and never lands in a commit. Restart
the add-in (Stop, then Run) and re-attach afterwards.
Most hash directories under a channel are partial. webdeploy keeps
incremental delta payloads beside the real installs, and they are hard to tell
apart from outside: a partial directory still has an Autodesk Fusion.app with
a Contents/. Only a few carry the full Api/Python subtree.
On 2026-09-03 pre-production held 8 hash directories and exactly one
was complete — and both .env and .zed/settings.json were pointing at a
partial one (8d5cf31c…), so the debug PYTHONPATH resolved to nothing. That
fails silently: no import error, just unresolved adsk.* stubs and breakpoints
that never trip.
So the script only considers a build a candidate when both artifacts exist
— Api/Python/packages/adsk and the bundled interpreter — which are exactly
the two things the configs need. Among those it takes the most recently
deployed, by app-bundle mtime. It prints the Fusion version beside each
candidate so you can sanity-check the ordering; the version is displayed only,
never used to sort, because it is not readable on every layout.
Two related traps:
- A hash is not unique to a channel.
8d5cf31c…currently appears under bothproduction/andpre-production/, so you cannot tell which build you are attached to from the hash alone. Pick the channel explicitly. developandfeature--…channels are included if they have a complete build;metais skipped, as it is webdeploy bookkeeping rather than a Fusion install.
The script is stdlib-only and probes for the webdeploy root instead of
hardcoding it. The macOS layout is verified; the Windows candidates are
unverified, and if none match it exits with what it tried rather than writing a
guess into your config. If you hit that on g16win.local, print the real tree
and add its relative paths to API_RELS / PYTHON_RELS.
The add-in starts the debugpy server only when debug mode is on, gated on
the git-ignored .debug marker file in the repository root
(config.WAIT_FOR_DEBUGGER = config.DEBUG):
# from the repository root
touch .debug # enable debug mode (verbose logging + debug server)
rm .debug # disableThe relevant config.py flags:
WAIT_FOR_DEBUGGER = DEBUG # tied to the .debug marker
DEBUGGER_PORT = 5678 # the in-process server port
DEBUGGER_BLOCK_UNTIL_ATTACHED = False # True → run() blocks until a debugger attachesPowerTools.py starts the server in _maybe_start_debug_server(), called at the
top of run(). It is non-fatal: if debugpy is missing or the listener
cannot start, it logs a warning and the add-in still loads. It also uses
in_process_debug_adapter=True (trap #2) and tolerates a
double listen() (trap #4).
Set DEBUGGER_BLOCK_UNTIL_ATTACHED = True only when you need to debug startup
itself — run() will then pause until an editor attaches.
VS Code uses Fusion's native integration; you do not need the .debug marker
for this path.
- In Fusion: Utilities › Add-Ins (Shift+S) → Add-Ins tab → select
PowerTools → click Debug. Fusion launches VS Code and generates
.vscode/launch.json("Python: Attach", port 9000) the first time. - VS Code attaches automatically. Set breakpoints, then invoke a command from the Fusion toolbar; execution stops at the breakpoint.
- Use F10 / F11 to step, and the Variables / Watch panels to inspect.
To reload after an edit: Stop the add-in in Scripts and Add-Ins, then Debug again (Python module caching means a fresh attach is required).
Alternative — attach VS Code to the in-process server (port 5678). If you prefer the Zed-style model in VS Code, create the
.debugmarker, Run the add-in (not Debug), and add an attach configuration to.vscode/launch.jsonpointing atlocalhost:5678. This avoids the Debug-button contamination in trap #4.
Open the repository in Zed (zed ~/Source/PowerTools). The repo already ships a
.zed/debug.json with an Attach to Fusion (PowerTools) configuration
(connect to 127.0.0.1:5678).
Per-session workflow:
- Ensure the
.debugmarker exists (touch .debug). - In Fusion: Scripts and Add-Ins → select PowerTools → Run (not Debug — Debug forces VS Code).
- Verify the server is listening:
Exactly one Fusion PID should appear.
lsof -nP -iTCP:5678 -sTCP:LISTEN
- In Zed: press F4 → choose Attach to Fusion (PowerTools). The session connects; threads and the call stack populate.
- Set breakpoints, invoke commands from Fusion, step, and inspect.
Reload cycle after a code edit: in Scripts and Add-Ins click Stop, then Run, then re-attach in Zed (F4). Python module caching means you cannot simply continue — a fresh attach is required.
Symptom. The debug session never starts and Zed's log
(~/Library/Logs/Zed/Zed.log) shows:
ERROR [debugger_ui::debugger_panel] debugpy installation failed (could not fetch Debugpy's wheel)
Cause. Unlike VS Code, Zed's Debugpy adapter runs its own copy of
debugpy (a local DAP bridge) that it installs by downloading a wheel from PyPI
into ~/Library/Application Support/Zed/debug_adapters/Debugpy/. Zed
recreates that folder on every attempt, so copying debugpy into it — or
into the project .venv — does not help on its own. Behind a corporate proxy (or
fully offline) the download fails and the session aborts.
Fix — point Zed at an existing debugpy so it skips the download. In
.zed/settings.json, set dap.Debugpy.binary to the debugpy/adapter directory
of any installed debugpy:
{
"lsp": { "…": "…" },
"dap": {
"Debugpy": {
"binary": "/Users/you/Library/Python/3.14/lib/python/site-packages/debugpy/adapter"
}
}
}Zed then runs <python3> <that path> … instead of fetching (confirmed in Zed's
dap_store.rs → python.rs: a set binary short-circuits the install — note
this is a settings.json key, not a debugAdapterPath field in
debug.json, which the code does not read). The adapter is pure Python and adds
its own site-packages to sys.path, so it runs under whatever python3 Zed
picks, regardless of the interpreter that built the debugpy you point at.
Use debugpy from Fusion's user site (the same one the in-process server uses,
maintained by setup-fusion-debug.sh) or from the repo .venv. The
setup-fusion-debug.sh / zed-enable-addin.sh scripts now emit this
dap.Debugpy.binary line automatically.
In Zed's F4 picker choose the labeled Attach to Fusion (PowerTools) entry. The auto-generated per-file / project-root scenarios lack this config and will still try to download.
| Purpose | Host | Port | Where configured |
|---|---|---|---|
VS Code (Fusion Debug button, injected debugpy) |
localhost | 9000 | .vscode/launch.json |
Zed / manual attach (in-process debugpy) |
127.0.0.1 | 5678 | .zed/debug.json + config.DEBUGGER_PORT |
The two ports are independent, so the two workflows do not collide — but do not launch via the Debug button and expect the in-process server in the same session (see trap #4).
These are the non-obvious failures encountered wiring up the Zed workflow
(documented in full in schneik80/Zed_Debug):
- Fusion's bundled Python ships without
pip.python -m pip install …fails with No module named pip. Bootstrap withpython -m ensurepip --user --upgradefirst (the setup script does this). - macOS launches a second Fusion.
debugpy.listen()spawns the adapter viasys.executable, which lives insideFusion.app— LaunchServices then "opens" the app and a second Fusion starts. Fix: passin_process_debug_adapter=True(done inPowerTools.py). Do not try to redirect the adapter to an external Python. - Zed uses
connect, nottcp_connection. Thetcp_connectionkey is silently ignored by Zed's Debugpy adapter (symptom: process exited before debugger attached). Use"connect": { "host": "127.0.0.1", "port": 5678 }. - The VS Code Debug button contaminates the process. If Fusion was launched
via Debug this session, VS Code has already injected and
listen()-ed its owndebugpy, so ourlisten()raises already called.PowerTools.pycatches thisRuntimeError. If it gets in the way, fully quit Fusion (⌘Q) and relaunch via Finder/Spotlight — not the Debug button.
A fifth, environment-specific trap — Zed failing to download debugpy behind a
proxy — has its own section:
When Zed cannot fetch debugpy.
- Attach works.
.debugpresent → Stop/Run the add-in → F4 in Zed (or Debug in VS Code) → session connects, threads + call stack populate. - Breakpoint binds. Set a breakpoint in a command's
command_created(e.g.commands/roundsketchdimensions/entry.py), invoke it from the toolbar → execution stops, the Variables panel shows the command args. - Watch evaluates. Add a watch on
adsk.core.Application.get()→ resolves to the running application object. - Step works. F10 / F11 step over / into.
- Reload cycle. Stop, edit a string, Run, re-attach → the breakpoint hits and the new value is visible.
- Ship-mode toggle. Remove
.debug, relaunch Fusion, confirmlsof -nP -iTCP:5678 -sTCP:LISTENis empty and the add-in runs normally.
Delete the .debug marker. config.DEBUG and config.WAIT_FOR_DEBUGGER both
become False, _maybe_start_debug_server() returns immediately, and no port is
opened. Because the marker is git-ignored, a distribution never contains it, so a
shipped build is always in ship mode. Confirm with:
lsof -nP -iTCP:5678 -sTCP:LISTEN # should return nothingA Fusion auto-update rotates the webdeploy hash, which breaks the absolute
paths in .zed/settings.json / .env and can wipe debugpy. Symptoms: pyright
stops resolving adsk.*, lsof no longer shows port 5678, or
ModuleNotFoundError: debugpy appears in the Text Commands log.
For the rotated paths, repoint both configs at the newest complete build of whichever channel you are running — this is the normal fix and it respects the channel you chose:
python3 tools/debug/update_debug_path.py pre-productionSee Pointing the config at a build.
Only if debugpy itself is gone do you need the upstream setup script,
which also rewrites the paths — minding the
channel caveat, since it scans production:
~/Source/Zed_Debug/scripts/setup-fusion-debug.sh /Users/schneik/Source/PowerToolsThe debugger is no help for the failure mode this add-in hits most: a native
fault. When Fusion segfaults (0xC0000005 on Windows, SIGSEGV on macOS)
there is no Python exception and no traceback, so nothing appears in the
DEBUG log and the breakpoint never trips. Absence of a traceback is not
evidence that a handler ran.
Fusion writes a Customer Error Report instead. On macOS:
ls -t ~/Library/Application\ Support/Autodesk/CER/*/*/ | head
# newest folder holds crashLog.txt.dmp.zip
unzip -o <path>/crashLog.txt.dmp.zip -d /tmp/cer && sed -n '1,120p' /tmp/cer/crashLog.txtOn Windows the reports live under
%LOCALAPPDATA%\Autodesk\CER\. Read the top of the stack; two frame patterns
in this codebase name their cause directly:
| Stack contains | Cause |
|---|---|
Xl::APICommandDefinitionImpl::doOnCreateCommand beneath createCommand ← Nu::CommandMgr::executeCommand |
Something re-entered the command manager from command_created — almost always an args.command.doExecute() call. Use commands/_command_abort.py instead (14871d7) |
| A fault after a long save/close run | A Document/Design handle held across a pumped wait went stale. Re-acquire after every wait; check isValid before closing (a1d22e1) |
Two habits that have saved real time here:
- Read the log and the crash stack before theorising. Two identical-looking "Preferences needs a document" bugs had different root causes, and each was settled by the log line or CER frame that pinned it.
- When the user hands you a crash zip path, open it. It is faster and more reliable than reasoning about which of several recent changes was responsible.
The full symptom-to-cause table, including the non-crashing failure modes, is
.agent/symptom-index.md.
- Developer Guide — setup, layout, tooling, the
.debugmarker. .agent/symptom-index.md— symptom → cause → commit..agent/environment.md— Fusion paths, API stubs, MCP servers.- Architecture — add-in lifecycle and the command model.
schneik80/Zed_Debug— the upstream recipe, helper scripts, and full write-up of the four traps.
Copyright © 2026 IMA LLC. All rights reserved.