Skip to main content

Attach-First Debugging

Using VS Code as a UI, Not an Orchestrator

Modern editors — VS Code especially — want to “run your program for you.” They want to:

  • select your runtime
  • activate your environment
  • construct your execution command
  • inject extensions
  • modify the environment
  • control stdout/stderr
  • wrap your program in debugging shims

In many cases, this is convenient. But when you care about correctness, reproducibility, or a clean runtime environment, this is the wrong choice.

The simple truth:

**VS Code should not be responsible for launching your program.

VS Code should only attach to it.**

This is the attach-first debugging philosophy:

✔ Run your program in a clean, trusted, external shell

✔ Let VS Code attach to it purely as a UI

✘ Do not let VS Code orchestrate your runtime environment

You get the best of both worlds:

  • full correctness (from clean external shell)
  • full debugging UI (from VS Code attach mode)

This approach avoids almost every problem caused by the “dirty shell” environment inside VS Code.


Why Launch-Mode Debugging Fails You

When VS Code launches your program, it:

  • injects environment variables
  • inherits VS Code’s polluted environment
  • uses extension-modified PATH
  • runs inside a pseudo-terminal parent
  • may reuse stale state from previous shells
  • may activate the wrong environment/runtime
  • may use the wrong interpreter
  • may wrap your program in a debug shim incorrectly

You think your program is running in the environment you configured. It is not.

VS Code becomes the source of truth for your runtime, which is almost always the wrong choice.


Why Attach-Mode Debugging Solves This

When you run your program externally and VS Code only attaches:

✔ Your environment is correct

It’s your OS environment, not VS Code's.

✔ PATH, variables, aliases, and tools come from your real shell

Not from the pseudo-terminal sandbox.

✔ No extension overrides or VS Code environment injection

All runtime decisions come from the external shell.

✔ You control how the program is launched

No editor magic, no hidden flags.

✔ VS Code becomes a passive observer

Not an active orchestrator.

Attach-first debugging reverts VS Code to what it should be: a UI, not an execution environment.


The Attach-First Workflow (Universal Pattern)

This is the same across languages:

1. Open a clean external terminal (Ctrl + Alt + T)
2. Start your program with "debug listening" enabled
3. VS Code: Start an "Attach" debugger

VS Code never launches your program. VS Code only attaches to an already-running process.

This guarantees:

  • reproducibility
  • clean environment
  • correct interpreter/runtime
  • correct PATH
  • correct version managers
  • no editor pollution

A Universal Debug Startup Command (Conceptual)

Most languages support:

<runtime> --debug / --inspect / --listen <port> my_program

And VS Code supports:

{
"request": "attach",
"type": "<language-server-type>",
"port": <port>,
"host": "localhost"
}

Once you understand this pattern, everything else is just syntax.


Attach-First Debugging by Language

Python (debugpy)

External terminal:

python -m debugpy --listen 5678 --wait-for-client my_script.py

VS Code launch.json:

{
"name": "Attach to Python",
"type": "python",
"request": "attach",
"connect": { "host": "localhost", "port": 5678 }
}

Node.js

External terminal:

node --inspect-brk=9229 app.js

VS Code:

{
"name": "Attach to Node",
"type": "node",
"request": "attach",
"port": 9229
}

Go (Delve)

External terminal:

dlv debug --headless --listen=:2345 --api-version=2 .

VS Code:

{
"name": "Attach to Go",
"type": "go",
"request": "attach",
"mode": "remote",
"port": 2345
}

C/C++ (gdbserver)

External terminal:

gdbserver localhost:7777 ./your_binary

VS Code:

{
"name": "Attach to C++",
"type": "cppdbg",
"request": "attach",
"MIMode": "gdb",
"miDebuggerServerAddress": "localhost:7777"
}

Rust (same as C++ via gdbserver or lldb-server)

Rust debugging uses the same pattern as C/C++.


Java (JPDA)

External terminal:

java -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005 Main

VS Code:

{
"type": "java",
"request": "attach",
"hostName": "localhost",
"port": 5005
}

Why Attach-First Is Objectively Superior

1. Reliability

Your clean external shell is always correct. VS Code’s terminal is not.

2. Determinism

Attach debugging produces identical behavior to running your program outside of VS Code.

3. Transparency

You see exactly how your program is launched. No hidden editor logic.

4. Portability

Your debugging setup works with:

  • TMUX
  • SSH sessions
  • Docker containers
  • Remote machines

5. Security

You avoid VS Code-inserted environment variables and IPC handles.

6. No “dirty shell” issues

Attach debugging bypasses VS Code’s contaminated environment entirely.


Attach-First Debugging Philosophy (One-Sentence Summary)

Always run your program in a clean environment you control. Use VS Code only as a UI lens for debugging — never as the launching authority.