This gist refers to ESP-IDF v5.5.2 - the current stable version as of Dec '25, paths will obviously change slightly in future versions.
The debugging toolchain works as follows:
- zed talks to gdb via it's Debug Adapter Protocol (dap) interface. This is configured in .zed/debug.json residing in your project directory. It's crucial to use a version of gdb built with the python extension in order for the dap interface to work - more on this later.
- gdb talks to openocd via it's built-in gdb server's tcp port (usually 3333).
- openocd talks to a jtag debugger (the newer ESP boards such as the S3 or C6 have a built-in ftdi jtag debugger)
- if your chip doesn't have a built-in debugger (e.g. esp32) the external debugger talks to your chip via it's jtag pins
So working backwards (I'm going to use the ESP32S3 as an example as it has the built-in jtag debugger):
- Plug your esp32s3 dev board into your USB port. (Some boards have 2 USB-C interfaces. Make sure to use the 'USB' labelled port which has the ftdi jtag interface & not the UART one.
ls /dev/cu.u*
/dev/cu.usbmodem1101
First let's create a simple shell script to fire up openocd.
# Start up openocd, usage: /usr/local/bin/openocd_start.sh <board_config_file>
#!/bin/zsh
cd ~/.espressif/tools/openocd-esp32
BASEDIR="$(pwd)/$(ls -t . | head -n1 )/openocd-esp32"
"$BASEDIR/bin/openocd" -s "$BASEDIR/scripts" $@
Save it somwehere convenient that's in your $PATH, I used /usr/local/bin/openocd_start.sh & make it executable with chmod +x
Now run:
openocd_start.sh -f board/esp32s3-builtin.cfg
Hopefully you should see openocd fire up, output some info about your board & end with:
Info : [esp32s3] Examination succeed
Info : [esp32s3] starting gdb server on 3333
Info : Listening on port 3333 for gdb connections
You can leave openocd running in it's own window for now, however you might later choose to create a Zed task for convenience.\
In your project's root folder create .zed/tasks.json & add the following:
// Project tasks configuration. See https://zed.dev/docs/tasks for documentation.
//
[
{
"label": "Start OpenOCD",
"command": "/usr/local/bin/openocd_start.sh -f board/esp32s3-builtin.cfg",
"use_new_terminal": true,
"reveal": "no_focus"
},
]
Test with cmd-shift-p to spawn the task (make sure you've first stopped the previous running instance of openocd)
Assuming openocd is running correctly, it's time to move on to gdb.
cd ~/.espressif/tools/xtensa-esp-elf-gdb/16.3_20250913/xtensa-esp-elf-gdb/bin
ls
You should see multiple versions of gdb, some with a number at the end, e.g. xtensa-esp-elf-gdb-3.13
These are versions of gdb built with python support - the number refers to the version of python required. As mentioned
ealier, we need a version of gdb with the dap interface, & that requires it was built with python support.
In order for gdb to run it needs to dynamically link to the python library libpython.3.xx.dylib so let's check if that works:
xtensa-esp-elf-gdb-3.13 --config
If you get the following:
dyld[13210]: Library not loaded: @executable_path/../lib/libpython3.13.dylib
It means that the macOS dynamic linker cannot find & load the libpython file. Unless you can find it on your system you will need to create it.
An easy way to do this is to use pyenv. This can be installed using HomeBrew & there are plenty of tutorials out there. Once you have it installed:
pyenv install 3.13
Will build python 3.13 & install it in your ~/.pyenv/versions folder.
Now we need to add a link from ~/.pyenv/versions/x.xx/lib/libpython.x.xx.dylib to somwehere gdb can find it.
cd ~/.espressif/tools/xtensa-esp-elf-gdb/16.3_20250913/xtensa-esp-elf-gdb/lib
ln -s ~/.pyenv/versions/3.13.11/lib/libpython3.13.dylib libpython3.13.dylib
cd ../bin
xtensa-esp-elf-gdb-3.13 --config
Hopefully you should now see a list of the configuration options gdb was built with. More importantly the following command should now work. (Exit with ctrl-d).
xtensa-esp-elf-gdb-3.13 -i dap
You have just connected to gdb through it's dap interface instead of the more usual command line interface. Ok now let's finally get Zed configured:
In your project's root folder create a .zed/debug.json file. Make sure to substitute for <user_name> & <app_name>.
// Project-local debug tasks
//
// For more documentation on how to configure debug tasks,
// see: https://zed.dev/docs/debugger
//
[
{
"label": "IDF: gdb via OpenOCD",
"adapter": "GDB",
"tcp_connection": {"port": 3333},
"env": {
"IDF_TARGET": "esp32s3",
"XTENSA_GNU_CONFIG" : "/Users/<user_name>/.espressif/tools/xtensa-esp-elf-gdb/16.3_20250913/xtensa-esp-elf-gdb/lib/xtensa_esp32s3.so",
},
"request": "launch",
"program": "$ZED_DIRNAME/build/<app_name>.elf",
"gdb_path": "/Users/<user_name>/.espressif/tools/xtensa-esp-elf-gdb/16.3_20250913/xtensa-esp-elf-gdb/bin/xtensa-esp-elf-gdb-3.13",
"gdb_args": [
"-x=build/gdbinit/symbols",
"-x=build/gdbinit/prefix-map",
"-x=build/gdbinit/py_extensions",
"-x=build/gdbinit/connect"
],
},
]
Annoyingly ~ expansion doesn't work in Zed at present so you must use /Users/<user_name> instead:
Save & press F4. All being well Zed should fire up the debugger, open up the file containing app_main & halt at the first line.
Congratulations! you've configured embedded debugging in Zed.
If your using the ESP32C6 for example all the above still apply but you need to substitute the~/.espressif/tools/riscv32-esp-elf-gdb/ path instead. There might not be a lib dir so you'll need to create one. There doesn't appear to be an equivalent XTENSA_GNU_CONFIG line for RISCV so just leave it out - seems to work ok.
Many thanks to bl-watts in the following discussion for pointing me in the right direction: [zed-industries/zed#31577]
Awesome, thank you so much for that detailed write-up! Would have taking me ages to pull that together. I tried for many hours to get it to go with Panic Nova. Had all the bits together, but could not quite get there.
Question: Do I have to take it all down and program the ESP using the usual method, or is there a way to use that JTAG connection to also program the ESP?
Anyway thanks so much Jon!