You open localhost:3000 and it's the wrong app. Or npm run dev says the port is taken, and lsof tells you it's node. Every dev server is node.
What you actually need is the folder it was started from. macOS doesn't have Linux's pwdx, but lsof can tell you, and it takes about ten seconds.
The quick answer
Swap 3000 for your port:
lsof -a -p "$(lsof -ti tcp:3000 -sTCP:LISTEN | head -1)" -d cwd -Fn | sed -n 's/^n//p'
It prints the working directory of whatever is listening on the port, like /Users/you/Code/acme-web. The rest of this post is what each part does, how to get the branch and the command too, and what to do when the answer is just /.
Step 1: find the PID
lsof -nP -iTCP:3000 -sTCP:LISTEN
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
node 48211 you 23u IPv6 0x7e9… 0t0 TCP *:3000 (LISTEN)
-n and -P skip the DNS and port-name lookups, so it answers instantly and shows 3000 instead of hbci. -sTCP:LISTEN keeps only the server. Without it you also get every client connected to the port, including your browser tab.
Add -t when you only want the number: lsof -ti tcp:3000 -sTCP:LISTEN prints 48211.
Step 2: ask lsof for its working directory
Every process has a current directory, and lsof lists it as a file descriptor called cwd:
lsof -a -p 48211 -d cwd
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
node 48211 you cwd DIR 1,18 640 382905693 /Users/you/Code/acme-web
-p picks the process, -d cwd picks the descriptor, and -a means both must match. Leave out -a and lsof ORs them, so you get every process's cwd on the machine.
For scripts, -Fn prints just the path with an n in front, which sed -n 's/^n//p' strips off. That's the quick answer above.
For most dev servers this is the project folder, because npm run dev, pnpm dev, rails s and go run . all run where you typed them.
Step 3: see the full command
ps -o command= -p 48211
This shows the whole command line, which often holds the path as well: node /Users/you/Code/acme-web/node_modules/.bin/vite. It also tells two servers in the same folder apart, like vite and storybook.
To see when it started, which helps spot the one you left running yesterday:
ps -o lstart= -p 48211
Step 4: get the git branch
git -C /Users/you/Code/acme-web branch --show-current
-C runs git as if you were in that folder, so you don't have to cd. This matters more than it used to. If you keep several worktrees of one repo, or a coding agent runs servers in its own worktree, you can have three copies of the same app on three ports. The folder and the branch are the only things that tell them apart.
Two more that help here:
-
git -C <folder> rev-parse --show-toplevelgives the repo root when the server runs in a subfolder, likepackages/webin a monorepo. -
git -C <folder> worktree listshows every worktree of that repo and its branch.
Make it one command
Add this to ~/.zshrc, open a new terminal, and run whoport 3000:
whoport() {
local pid dir
pid=$(lsof -ti tcp:"$1" -sTCP:LISTEN | head -1)
[[ -z $pid ]] && { echo "Nothing is listening on :$1"; return 1; }
dir=$(lsof -a -p "$pid" -d cwd -Fn | sed -n 's/^n//p')
echo "PID $pid"
echo "Command $(ps -o command= -p "$pid")"
echo "Folder $dir"
git -C "$dir" rev-parse --is-inside-work-tree &>/dev/null &&
echo "Branch $(git -C "$dir" branch --show-current)"
}
% whoport 3000
PID 48211
Command node /Users/you/Code/acme-web/node_modules/.bin/vite
Folder /Users/you/Code/acme-web
Branch feature/search
To see every listening server with its folder at once:
lsof -tiTCP -sTCP:LISTEN | sort -u | while read pid; do
printf '%-6s %-18.18s %s\n' "$pid" "$(ps -o comm= -p "$pid" | sed 's#.*/##')" \
"$(lsof -a -p "$pid" -d cwd -Fn | sed -n 's/^n//p')"
done
You'll see a few apps of your own in there too, like your editor's helpers. Their folder is usually /.
If you check this more than once a day: full disclosure, I built Portlivo, a Mac menu bar app that shows every dev server with its project, git branch and port. Press E on one and its folder opens in VS Code, Cursor or Zed. It's $8.99 once for the first 50 licences, after a 7-day free trial. The commands above are free and enough if you only need this now and then.
Open it in your editor
Once you have the folder, one command opens it:
| Editor | Command | Install the command |
|---|---|---|
| VS Code | code ~/Code/acme-web |
Command Palette → Shell Command: Install 'code' command in PATH |
| Cursor | cursor ~/Code/acme-web |
Command Palette → Shell Command: Install 'cursor' command |
| Zed | zed ~/Code/acme-web |
Zed menu → Install CLI |
| Any app | open -a "Visual Studio Code" ~/Code/acme-web |
Nothing to install |
Put them together and you jump from a port straight to its code:
code "$(lsof -a -p "$(lsof -ti tcp:3000 -sTCP:LISTEN | head -1)" -d cwd -Fn | sed -n 's/^n//p')"
When the folder is / or makes no sense
Some servers aren't started from a project folder. lsof is still right; the folder just isn't the useful answer.
| What you see | What it is | Where to look instead |
|---|---|---|
com.docker.backend or OrbStack holds the port |
A container |
docker ps --filter publish=3000, then the Compose label below |
Folder is /, command is postgres, redis-server or mysqld
|
A Homebrew service, started by launchd |
brew services list. It isn't tied to a project. |
Folder is /, command is nginx
|
Laravel Herd or Valet |
herd links or valet links list the folder behind each .test site |
Folder ends in /public
|
php artisan serve, which runs from the public folder |
git -C <folder> rev-parse --show-toplevel for the project root |
Folder is your home folder or /, process is node
|
Started by pm2 or another process manager |
pm2 list, then pm2 describe <name> shows its exec cwd |
| lsof prints nothing, but the port is taken | A process owned by root or another user | Run the same commands with sudo
|
For a Docker Compose container, this prints the folder of the project that started it:
docker inspect <container> | grep working_dir
"com.docker.compose.project.working_dir": "/Users/you/Code/acme-web",
If the port is taken and you want it back, I wrote up how to stop the process on a port safely, including the ones that come straight back.
FAQ
Is there a pwdx command on Mac?
No. pwdx and /proc/<PID>/cwd are Linux only. On macOS, lsof -a -p <PID> -d cwd does the same job.
How do I find which port a project is using?
Run the loop from "Make it one command", which lists every listening server with its folder, and look for yours. Or check the project's config: Vite defaults to 5173, Next.js and Rails to 3000, Laravel to 8000.
Does this work for Python, Ruby, Go and PHP servers?
Yes. lsof and ps don't care what language the process is in. I tested the steps above with python3 -m http.server.
Why does lsof show nothing for a port I know is in use?
Usually the process belongs to another user, so try sudo. If it's a Docker container, lsof shows Docker's own process, not the app inside it.
Can I see this without the terminal?
Activity Monitor shows a process's open files and ports (double-click it, then Open Files and Ports), and the cwd is in that list. It's slower for this than lsof, but it works.
The bottom line
lsof finds the PID, lsof -d cwd turns it into a folder, and git -C gives you the branch. Put them in a whoport function and "which project is this?" takes one command.
How many dev servers do you usually have running at once?
I'm Anmol. I build Mac apps and write about Mac productivity. Follow me on X at @anmolsethi_dev.
Top comments (0)