<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Anmol Sethi</title>
    <description>The latest articles on DEV Community by Anmol Sethi (@anmolsethi_dev).</description>
    <link>https://dev.to/anmolsethi_dev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4191890%2F72f696a4-82f3-4b00-8773-b5ff3e4e19b4.jpg</url>
      <title>DEV Community: Anmol Sethi</title>
      <link>https://dev.to/anmolsethi_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anmolsethi_dev"/>
    <language>en</language>
    <item>
      <title>Mac Text Replacement Not Working? Here's Every Fix</title>
      <dc:creator>Anmol Sethi</dc:creator>
      <pubDate>Sun, 08 Nov 2026 18:36:40 +0000</pubDate>
      <link>https://dev.to/anmolsethi_dev/mac-text-replacement-not-working-heres-every-fix-2d9j</link>
      <guid>https://dev.to/anmolsethi_dev/mac-text-replacement-not-working-heres-every-fix-2d9j</guid>
      <description>&lt;p&gt;You set up &lt;code&gt;omw&lt;/code&gt; to type “On my way!” and it works in Messages. Then you try it in Slack, or Word, or a Google Doc, and nothing happens.&lt;/p&gt;

&lt;p&gt;Text Replacements are the Mac’s built-in text expander, and they fail for a small number of reasons. Here’s each one, in the order worth checking.&lt;/p&gt;

&lt;h2&gt;
  
  
  The quick answer
&lt;/h2&gt;

&lt;p&gt;Try the shortcut in TextEdit first. If it expands there, your replacement is fine and the app you were in either has the feature switched off (&lt;strong&gt;Edit → Substitutions → Text Replacement&lt;/strong&gt;) or doesn’t use Apple’s text system at all. If it doesn’t expand in TextEdit either, check the shortcut’s spelling in &lt;strong&gt;System Settings → Keyboard → Text Replacements…&lt;/strong&gt; and remember it only expands after you type a space or punctuation.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Test it in TextEdit
&lt;/h2&gt;

&lt;p&gt;This one step tells you which half of the problem you have. Open TextEdit, make a new document, type your shortcut and press Space.&lt;/p&gt;

&lt;p&gt;If it expands, the replacement itself is fine, and the rest of this article is about the app you were using. If it doesn’t, the problem is the replacement or the Mac, so go to step 3.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Turn it on in the app
&lt;/h2&gt;

&lt;p&gt;Text Replacement is a per-app switch in every app that has an Edit menu built on Apple’s text system: TextEdit, Mail, Notes, Pages, Messages and many others. It can be off in one app and on in the rest.&lt;/p&gt;

&lt;p&gt;Click into a text field, then open &lt;strong&gt;Edit → Substitutions&lt;/strong&gt; and make sure &lt;strong&gt;Text Replacement&lt;/strong&gt; has a tick. While you’re there, &lt;strong&gt;Show Substitutions&lt;/strong&gt; lists every switch for that app in one small window.&lt;/p&gt;

&lt;p&gt;If there’s no Substitutions menu at all, the app draws its own text fields. Skip to step 5.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Check the shortcut itself
&lt;/h2&gt;

&lt;p&gt;Open &lt;strong&gt;System Settings → Keyboard&lt;/strong&gt; and click &lt;strong&gt;Text Replacements…&lt;/strong&gt;. Then check three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Spelling.&lt;/strong&gt; The shortcut has to match exactly what you type. A stray space at the end is easy to miss.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No spaces inside.&lt;/strong&gt; A shortcut is one word. &lt;code&gt;my addr&lt;/code&gt; won’t work; &lt;code&gt;myaddr&lt;/code&gt; will.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not a real word.&lt;/strong&gt; If your shortcut is a word you also type for real, like &lt;code&gt;addr&lt;/code&gt; or &lt;code&gt;sig&lt;/code&gt;, you’ll get the replacement when you don’t want it. Start shortcuts with a character you never type, like &lt;code&gt;;&lt;/code&gt; or &lt;code&gt;@@&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Finish the word
&lt;/h2&gt;

&lt;p&gt;A Text Replacement doesn’t fire when you finish typing the shortcut. It fires on the space or punctuation after it. Type &lt;code&gt;omw&lt;/code&gt; and stop, and nothing happens. Type &lt;code&gt;omw&lt;/code&gt; and a space, and it expands.&lt;/p&gt;

&lt;p&gt;In some apps it shows as a suggestion instead: a small bubble under the word that you accept with Space, or dismiss with Esc. If you press Esc out of habit, you’ve turned it down.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The apps that never expand them
&lt;/h2&gt;

&lt;p&gt;Text Replacements are part of Apple’s text system, so they only work in text fields that use it. Plenty of popular apps draw their own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Microsoft Word, Excel and PowerPoint.&lt;/strong&gt; They use Office’s own AutoCorrect instead. Add the same shortcut in Word under &lt;strong&gt;Tools → AutoCorrect&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Docs.&lt;/strong&gt; It has its own list under &lt;strong&gt;Tools → Preferences → Substitutions&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Electron apps&lt;/strong&gt;, such as Slack, Discord, VS Code, Notion and Obsidian. Some pick up the Mac’s spelling, but your replacements usually don’t expand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terminals&lt;/strong&gt;: Terminal, iTerm2, Ghostty and the rest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web apps&lt;/strong&gt; with their own editors, like many chat and email tools in the browser. A plain text box on a web page often works in Safari, but a fancy editor usually doesn’t.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There’s no setting on the Mac that changes this. You either add the shortcut again inside the app (Word, Google Docs) or use a text expander that types into any app.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. When they don’t reach your iPhone
&lt;/h2&gt;

&lt;p&gt;Text Replacements sync through iCloud, and when they don’t arrive it’s usually one of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Both devices must be signed in to the same Apple Account, with iCloud on.&lt;/li&gt;
&lt;li&gt;Syncing can take a while. Add one test replacement on the Mac, and give it time before you change anything else.&lt;/li&gt;
&lt;li&gt;If one device never gets them, signing out of iCloud and back in on that device is the usual last resort. Make sure your other iCloud data is synced first.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  7. See every replacement from Terminal
&lt;/h2&gt;

&lt;p&gt;The Settings list is fine for ten replacements and painful for fifty. Your Mac keeps them in a small database you can read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sqlite3 ~/Library/KeyboardServices/TextReplacements.db &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"select ZSHORTCUT, ZPHRASE from ZTEXTREPLACEMENTENTRY where ZWASDELETED = 0;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It prints one replacement per line, shortcut first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;;addr|221B Baker Street, London NW1 6XE
omw|On my way!
;sig|Thanks, Anmol
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mine listed 10 (those three are examples, not mine). Change the end to &lt;code&gt;select count(*) from ZTEXTREPLACEMENTENTRY where ZWASDELETED = 0;&lt;/code&gt; to get only the number. It’s a quick way to spot a typo in a shortcut, or a duplicate where two shortcuts fight. Only read it: changes made here don’t sync, so edit replacements in System Settings.&lt;/p&gt;

&lt;p&gt;To back them up, select them all in the Text Replacements window (⌘A) and drag them to the desktop. You get a &lt;code&gt;.plist&lt;/code&gt; file you can drag back in on any Mac.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Text Replacements aren’t enough
&lt;/h2&gt;

&lt;p&gt;If most of your typing happens in Slack, a terminal or Word, step 5 is the wall you keep hitting. That’s where a text expander takes over, because it watches the keyboard instead of relying on each app’s text system.&lt;/p&gt;

&lt;p&gt;Full disclosure, I built one: &lt;a href="https://snipora.app/?utm_source=devto" rel="noopener noreferrer"&gt;Snipora&lt;/a&gt;. It reads your Text Replacements the first time it opens and expands them in every app, Electron apps and terminals included, and Esc undoes an expansion you didn’t want. It’s free for 15 snippets; the lifetime licence is $8.99 for the first 50 buyers, then $39.99. If your replacements only need to work in Apple’s apps, you don’t need it: the built-in ones are free and sync to your iPhone.&lt;/p&gt;

&lt;p&gt;Back to the built-in ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  One habit that prevents most of this
&lt;/h2&gt;

&lt;p&gt;Start every shortcut with the same character. I use &lt;code&gt;;&lt;/code&gt;: &lt;code&gt;;addr&lt;/code&gt;, &lt;code&gt;;sig&lt;/code&gt;, &lt;code&gt;;mtg&lt;/code&gt;. You’ll never type it by accident, you can see all of them at a glance in the Settings list, and when one stops working, you know it’s the app and not a clash with a real word.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Where are Text Replacements on a Mac?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
System Settings → Keyboard → Text Replacements…. Click + to add one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does my text replacement work in Notes but not in Word?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Word uses Office’s own AutoCorrect, not the Mac’s list. Add the shortcut under Tools → AutoCorrect in Word.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why don’t text replacements work in Slack or VS Code?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
They’re Electron apps that draw their own text fields, so Apple’s text system never sees what you type. Nothing in System Settings changes that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can a text replacement contain today’s date?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
No. Text Replacements are fixed text. A Shortcuts keyboard shortcut or a text expander can type the date.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can a text replacement have more than one line?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Yes, but you can’t type a line break in the Settings field. Write the text in TextEdit, copy it, and paste it into the Replace With field.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;Test in TextEdit first. If it works there, the app is the problem: switch it on under Edit → Substitutions, or add the shortcut to the app’s own list. If it doesn’t, check the spelling, and remember the space at the end.&lt;/p&gt;

&lt;p&gt;Which app do your text replacements fail in most? Tell me in the comments.&lt;/p&gt;

&lt;p&gt;I’m Anmol. I build Mac apps and write about Mac productivity. Follow me on X at &lt;a href="https://x.com/anmolsethi_dev" rel="noopener noreferrer"&gt;@anmolsethi_dev&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>writing</category>
    </item>
    <item>
      <title>How to Test Your Local Dev Server on Your Phone</title>
      <dc:creator>Anmol Sethi</dc:creator>
      <pubDate>Thu, 05 Nov 2026 15:14:44 +0000</pubDate>
      <link>https://dev.to/anmolsethi_dev/how-to-test-your-local-dev-server-on-your-phone-56nd</link>
      <guid>https://dev.to/anmolsethi_dev/how-to-test-your-local-dev-server-on-your-phone-56nd</guid>
      <description>&lt;p&gt;Your Vite app loads on your phone. Then every API call fails, and the page sits there with a spinner.&lt;/p&gt;

&lt;p&gt;Nothing is wrong with your code. On the phone, &lt;code&gt;localhost&lt;/code&gt; is the phone. Here's how to open a dev server from your Mac on your phone, and make the API calls work too.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Let Vite listen on your network: &lt;code&gt;server.host: true&lt;/code&gt; in &lt;code&gt;vite.config.js&lt;/code&gt; (or &lt;code&gt;vite --host&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Open the Network address Vite prints, on a phone on the same Wi-Fi.&lt;/li&gt;
&lt;li&gt;Proxy your API through Vite and call &lt;code&gt;/api/...&lt;/code&gt; instead of &lt;code&gt;http://localhost:3001/api/...&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The rest of this post is why each step is needed, and what to do when one of them doesn't work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: let the dev server listen on your network
&lt;/h2&gt;

&lt;p&gt;Run &lt;code&gt;npm run dev&lt;/code&gt; in a fresh Vite project and you get this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vite listens on localhost only, on purpose: nothing outside your Mac can reach a dev server by accident. To open it from your phone, tell it to listen on every network interface. You can pass &lt;code&gt;--host&lt;/code&gt; each time, but putting it in the config means you never forget:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vite.config.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start it again and Vite prints a second address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  ➜  Local:   http://localhost:5173/
  ➜  Network: http://192.168.1.33:5173/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;192.168.1.33&lt;/code&gt; is your Mac's address on your Wi-Fi. Yours will differ. If you need it outside Vite, this prints it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ipconfig getifaddr en0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On some Macs Wi-Fi is &lt;code&gt;en1&lt;/code&gt;, not &lt;code&gt;en0&lt;/code&gt;; if the first prints nothing, try the second.&lt;/p&gt;

&lt;p&gt;Type the Network address into Safari or Chrome on your phone. The phone has to be on the same Wi-Fi as the Mac; mobile data won't work, and neither will most guest networks or office networks that keep devices apart.&lt;/p&gt;

&lt;p&gt;If the page doesn't load at all:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The macOS firewall&lt;/strong&gt; may be blocking node. If it's on (System Settings → Network → Firewall), macOS asks whether to allow incoming connections the first time node listens on the network. Say Allow. If you clicked Deny once, find node in the firewall's Options list and change it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A VPN&lt;/strong&gt; on the Mac or the phone can route the request somewhere else. Turn it off for the test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You typed &lt;code&gt;localhost&lt;/code&gt;&lt;/strong&gt; on the phone out of habit. It has to be the IP.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 2: why your API calls fail on the phone
&lt;/h2&gt;

&lt;p&gt;This is the one that gets people second. The page loads, the styles load, hot reload even works, and then nothing comes back from the API.&lt;/p&gt;

&lt;p&gt;Look at how the app calls it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://localhost:3001/api/projects&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On your Mac, &lt;code&gt;localhost:3001&lt;/code&gt; is your API, so this works. On your phone, the same JavaScript runs in the phone's browser, and &lt;code&gt;localhost&lt;/code&gt; means the phone itself. Nothing listens on port 3001 on your phone, so the request fails. Vite isn't involved; the browser goes straight to the address in the code.&lt;/p&gt;

&lt;p&gt;There are a few ways around it. The cleanest one is to stop the browser from calling the API at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix: proxy the API through Vite
&lt;/h3&gt;

&lt;p&gt;Vite has a built-in proxy. You tell it which paths belong to the API, and it forwards those requests to the API on your Mac:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vite.config.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://localhost:3001&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then change the app to call relative paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/projects&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the phone only ever talks to Vite, at the Network address it already loaded the page from. Vite runs on your Mac, so when it forwards the request to &lt;code&gt;localhost:3001&lt;/code&gt;, that's your Mac's localhost, where your API is.&lt;/p&gt;

&lt;p&gt;Here's the result on an iPhone: the page comes from &lt;code&gt;192.168.1.33:5173&lt;/code&gt;, and the fetch to &lt;code&gt;/api/health&lt;/code&gt; comes back 200 from the API on port 3001 on the Mac.&lt;/p&gt;

&lt;p&gt;What this gets you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The same code on the Mac and the phone.&lt;/strong&gt; &lt;code&gt;/api/...&lt;/code&gt; works on &lt;code&gt;localhost:5173&lt;/code&gt; and on the Network address, with no environment variable to switch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No CORS.&lt;/strong&gt; The browser sees one origin, Vite's, so there's no cross-origin request to allow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The API stays private.&lt;/strong&gt; It can keep listening on localhost only; just Vite has to listen on the network.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your API's routes don't start with &lt;code&gt;/api&lt;/code&gt; (say the backend serves &lt;code&gt;/projects&lt;/code&gt; and the app calls &lt;code&gt;/api/projects&lt;/code&gt;), strip the prefix on the way through:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://localhost:3001&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;rewrite&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;api/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the backend is a NestJS app with &lt;code&gt;app.setGlobalPrefix("api")&lt;/code&gt;, its routes already start with &lt;code&gt;/api&lt;/code&gt; and the short version works as it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other ways, and when they're better
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Put the Mac's IP in the API URL.&lt;/strong&gt; For example &lt;code&gt;VITE_API_URL=http://192.168.1.33:3001&lt;/code&gt; and &lt;code&gt;fetch(import.meta.env.VITE_API_URL + "/projects")&lt;/code&gt;. It works, but the API now has to listen on the network too, it needs CORS for the phone's origin, and the IP changes when you switch networks or the router hands out a new one. Fine for a quick check; the proxy is less to keep in sync.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use a tunnel.&lt;/strong&gt; &lt;code&gt;cloudflared tunnel --url http://localhost:5173&lt;/code&gt; (Cloudflare) or &lt;code&gt;ngrok http 5173&lt;/code&gt; gives you a public HTTPS address. That's the one to use when the phone isn't on your Wi-Fi, when you want to send the link to someone else, or when you're testing anything a browser only allows over HTTPS: the camera, geolocation, service workers, Web Share. Keep the Vite proxy; the tunnel only needs to reach Vite. Vite may block the tunnel's hostname until you add it to &lt;code&gt;server.allowedHosts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A real device over USB.&lt;/strong&gt; Safari's Web Inspector (Develop menu → your iPhone) and Chrome's remote debugging for Android let you see the phone's console and network tab from the Mac. Turn it on once and you'll see the failed &lt;code&gt;localhost&lt;/code&gt; request straight away next time, instead of guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Typing the IP gets old
&lt;/h2&gt;

&lt;p&gt;You'll do this often enough that typing &lt;code&gt;192.168.1.33:5173&lt;/code&gt; on a phone keyboard gets tedious. Two shortcuts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In Chrome on the Mac, open the Network address, then the share menu (or right-click the page) → &lt;strong&gt;Create QR Code&lt;/strong&gt;, and scan it with the phone's camera.&lt;/li&gt;
&lt;li&gt;Bookmark the address on the phone. It stays right as long as your Mac keeps the same IP, which on a home network is usually true for a while.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also build a menu bar app for local dev servers, Portlivo (full disclosure, it's mine). It marks the servers your phone can reach and has a Show QR Code for Phone item for each one, so I scan instead of typing. Servers that only listen on localhost don't get one, which is a handy hint that &lt;code&gt;host: true&lt;/code&gt; is missing. It's at &lt;a href="https://portlivo.com/?utm_source=devto" rel="noopener noreferrer"&gt;portlivo.com&lt;/a&gt;. Back to the setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  A checklist for next time
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;server.host: true&lt;/code&gt; in &lt;code&gt;vite.config.js&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Phone on the same Wi-Fi, opened at the Network address, not &lt;code&gt;localhost&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Firewall allows node.&lt;/li&gt;
&lt;li&gt;API calls are relative (&lt;code&gt;/api/...&lt;/code&gt;) and &lt;code&gt;server.proxy&lt;/code&gt; sends them to the backend.&lt;/li&gt;
&lt;li&gt;Need HTTPS or a different network: a tunnel to Vite.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What's the first thing that broke the last time you tested a local app on your phone?&lt;/p&gt;

&lt;p&gt;I'm Anmol. I build Mac apps and write about Mac productivity. Follow me on X at &lt;a href="https://x.com/anmolsethi_dev" rel="noopener noreferrer"&gt;@anmolsethi_dev&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>vite</category>
      <category>javascript</category>
      <category>mobile</category>
    </item>
    <item>
      <title>How to kill the process on a port on a Mac (and what to do when it comes back)</title>
      <dc:creator>Anmol Sethi</dc:creator>
      <pubDate>Fri, 23 Oct 2026 13:30:00 +0000</pubDate>
      <link>https://dev.to/anmolsethi_dev/how-to-kill-the-process-on-a-port-on-a-mac-and-what-to-do-when-it-comes-back-3ni7</link>
      <guid>https://dev.to/anmolsethi_dev/how-to-kill-the-process-on-a-port-on-a-mac-and-what-to-do-when-it-comes-back-3ni7</guid>
      <description>&lt;p&gt;You kill whatever is on port 5432, run your app, and get the same error. The process is back with a new PID. Most guides stop at &lt;code&gt;kill -9&lt;/code&gt;, which is the wrong fix for this. Here's the full routine, in the order I use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-liner
&lt;/h2&gt;

&lt;p&gt;Find the process listening on the port and ask it to quit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lsof &lt;span class="nt"&gt;-ti&lt;/span&gt; tcp:3000 &lt;span class="nt"&gt;-sTCP&lt;/span&gt;:LISTEN | xargs &lt;span class="nb"&gt;kill&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-t&lt;/code&gt; prints only PIDs. &lt;code&gt;-sTCP:LISTEN&lt;/code&gt; is the part most snippets leave out, and it matters: without it, lsof also returns every client connected to the port. If you have the app open in a browser, that includes the browser, and you quit it instead of the server.&lt;/p&gt;

&lt;p&gt;A range of ports works too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lsof &lt;span class="nt"&gt;-ti&lt;/span&gt; tcp:3000-3010 &lt;span class="nt"&gt;-sTCP&lt;/span&gt;:LISTEN | xargs &lt;span class="nb"&gt;kill&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The careful version
&lt;/h2&gt;

&lt;p&gt;When you're not sure what's on the port, take it in four steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;See what's listening: &lt;code&gt;lsof -nP -iTCP:3000 -sTCP:LISTEN&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Check it's the right thing: &lt;code&gt;ps -p &amp;lt;PID&amp;gt; -o command=&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Ask it to quit: &lt;code&gt;kill &amp;lt;PID&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Check again after a second. Only if it's still there: &lt;code&gt;kill -9 &amp;lt;PID&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 2 is the one people skip. &lt;code&gt;node&lt;/code&gt; on port 3000 could be your app, or a different project you forgot was running.&lt;/p&gt;

&lt;h2&gt;
  
  
  kill, kill -INT or kill -9?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;When to use it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kill &amp;lt;PID&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SIGTERM&lt;/td&gt;
&lt;td&gt;First choice. The server closes files and connections, then exits.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kill -INT &amp;lt;PID&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SIGINT&lt;/td&gt;
&lt;td&gt;What Ctrl-C sends. Some servers only clean up on this one.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kill -9 &amp;lt;PID&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SIGKILL&lt;/td&gt;
&lt;td&gt;Last resort. It can't be caught, so nothing gets cleaned up.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A database killed with &lt;code&gt;-9&lt;/code&gt; has to recover on its next start, and a dev server may leave a lock or socket file behind. Give SIGTERM a second before reaching for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make it a command
&lt;/h2&gt;

&lt;p&gt;If you do this every day, add a function to &lt;code&gt;~/.zshrc&lt;/code&gt;, open a new terminal, and run &lt;code&gt;killport 3000&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;killport&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; lsof &lt;span class="nt"&gt;-ti&lt;/span&gt; tcp:&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-sTCP&lt;/span&gt;:LISTEN | xargs &lt;span class="nb"&gt;kill&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are npm tools too. &lt;code&gt;npx kill-port 3000&lt;/code&gt; sends SIGKILL straight away, with no grace period. &lt;code&gt;npx fkill-cli :3000&lt;/code&gt; stops the process on a port, and &lt;code&gt;npx fkill-cli&lt;/code&gt; on its own opens an interactive picker.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you do this several times a day: full disclosure, I built &lt;a href="https://portlivo.com/?utm_source=devto" rel="noopener noreferrer"&gt;Portlivo&lt;/a&gt;, a Mac menu bar app that lists every dev server and port. Select one and press K, and it stops it safely, including Homebrew services and Docker containers. It's $8.99 once (early-bird) after a 7-day free trial.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Stop it by name instead
&lt;/h2&gt;

&lt;p&gt;When you know what it is but not which port it took:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pgrep &lt;span class="nt"&gt;-lf&lt;/span&gt; &lt;span class="s2"&gt;"vite"&lt;/span&gt;    &lt;span class="c"&gt;# list matches first&lt;/span&gt;
pkill &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"vite"&lt;/span&gt;     &lt;span class="c"&gt;# then stop them&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-f&lt;/code&gt; matches anywhere in the command line, so &lt;code&gt;pkill -f node&lt;/code&gt; also stops your editor's helpers. Always run &lt;code&gt;pgrep -lf&lt;/code&gt; with the same pattern first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the port is free
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nc &lt;span class="nt"&gt;-z&lt;/span&gt; localhost 3000 2&amp;gt;/dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;taken &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;free
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it still says &lt;code&gt;taken&lt;/code&gt; a second after you stopped the process, something restarted it.&lt;/p&gt;

&lt;h2&gt;
  
  
  When it comes straight back
&lt;/h2&gt;

&lt;p&gt;That's a supervisor doing its job. Homebrew services run under launchd with KeepAlive, so a killed Postgres is restarted within a second. Killing it harder doesn't help. Stop it where it's managed:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What holds the port&lt;/th&gt;
&lt;th&gt;Stop it with&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Homebrew service (Postgres, MySQL, Redis)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;brew services stop &amp;lt;name&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docker or OrbStack container&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker stop &amp;lt;container&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pm2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pm2 stop &amp;lt;name&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nodemon or a file watcher&lt;/td&gt;
&lt;td&gt;Ctrl-C in the terminal that runs it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;launchd agent&lt;/td&gt;
&lt;td&gt;&lt;code&gt;launchctl bootout gui/$(id -u)/&amp;lt;label&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Laravel Herd (nginx, PHP, dnsmasq)&lt;/td&gt;
&lt;td&gt;Stop it from Herd&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Not sure which one it is? &lt;code&gt;brew services list&lt;/code&gt; and &lt;code&gt;docker ps&lt;/code&gt; cover most cases. For other launchd agents, look in &lt;code&gt;~/Library/LaunchAgents&lt;/code&gt; or run &lt;code&gt;launchctl list | grep &amp;lt;name&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Operation not permitted"
&lt;/h2&gt;

&lt;p&gt;The process belongs to root or another user. &lt;code&gt;sudo kill &amp;lt;PID&amp;gt;&lt;/code&gt; works, but check what it is first. One to leave alone: &lt;code&gt;ControlCe&lt;/code&gt; on port 5000 or 7000 is macOS's AirPlay Receiver. Turn it off in System Settings → General → AirDrop &amp;amp; Handoff instead.&lt;/p&gt;

&lt;p&gt;That covers every way I free a port. If it saved you a trip to Stack Overflow, follow me for more Mac developer fixes like this one.&lt;/p&gt;

</description>
      <category>macos</category>
      <category>terminal</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How to Find Which Project Is Running on a Port on Mac</title>
      <dc:creator>Anmol Sethi</dc:creator>
      <pubDate>Thu, 22 Oct 2026 04:54:49 +0000</pubDate>
      <link>https://dev.to/anmolsethi_dev/how-to-find-which-project-is-running-on-a-port-on-mac-3n5a</link>
      <guid>https://dev.to/anmolsethi_dev/how-to-find-which-project-is-running-on-a-port-on-mac-3n5a</guid>
      <description>&lt;p&gt;You open localhost:3000 and it's the wrong app. Or &lt;code&gt;npm run dev&lt;/code&gt; says the port is taken, and &lt;code&gt;lsof&lt;/code&gt; tells you it's &lt;code&gt;node&lt;/code&gt;. Every dev server is &lt;code&gt;node&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What you actually need is the folder it was started from. macOS doesn't have Linux's &lt;code&gt;pwdx&lt;/code&gt;, but lsof can tell you, and it takes about ten seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The quick answer
&lt;/h2&gt;

&lt;p&gt;Swap 3000 for your port:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lsof &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;lsof &lt;span class="nt"&gt;-ti&lt;/span&gt; tcp:3000 &lt;span class="nt"&gt;-sTCP&lt;/span&gt;:LISTEN | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; cwd &lt;span class="nt"&gt;-Fn&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'s/^n//p'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It prints the working directory of whatever is listening on the port, like &lt;code&gt;/Users/you/Code/acme-web&lt;/code&gt;. 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 &lt;code&gt;/&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: find the PID
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lsof &lt;span class="nt"&gt;-nP&lt;/span&gt; &lt;span class="nt"&gt;-iTCP&lt;/span&gt;:3000 &lt;span class="nt"&gt;-sTCP&lt;/span&gt;:LISTEN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;COMMAND   PID  USER   FD   TYPE  DEVICE SIZE/OFF NODE NAME
node    48211   you   23u  IPv6  0x7e9…      0t0  TCP *:3000 (LISTEN)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-n&lt;/code&gt; and &lt;code&gt;-P&lt;/code&gt; skip the DNS and port-name lookups, so it answers instantly and shows &lt;code&gt;3000&lt;/code&gt; instead of &lt;code&gt;hbci&lt;/code&gt;. &lt;code&gt;-sTCP:LISTEN&lt;/code&gt; keeps only the server. Without it you also get every client connected to the port, including your browser tab.&lt;/p&gt;

&lt;p&gt;Add &lt;code&gt;-t&lt;/code&gt; when you only want the number: &lt;code&gt;lsof -ti tcp:3000 -sTCP:LISTEN&lt;/code&gt; prints &lt;code&gt;48211&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: ask lsof for its working directory
&lt;/h2&gt;

&lt;p&gt;Every process has a current directory, and lsof lists it as a file descriptor called &lt;code&gt;cwd&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lsof &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 48211 &lt;span class="nt"&gt;-d&lt;/span&gt; cwd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;COMMAND   PID USER   FD   TYPE DEVICE SIZE/OFF      NODE NAME
node    48211  you  cwd    DIR   1,18      640 382905693 /Users/you/Code/acme-web
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-p&lt;/code&gt; picks the process, &lt;code&gt;-d cwd&lt;/code&gt; picks the descriptor, and &lt;code&gt;-a&lt;/code&gt; means both must match. Leave out &lt;code&gt;-a&lt;/code&gt; and lsof ORs them, so you get every process's cwd on the machine.&lt;/p&gt;

&lt;p&gt;For scripts, &lt;code&gt;-Fn&lt;/code&gt; prints just the path with an &lt;code&gt;n&lt;/code&gt; in front, which &lt;code&gt;sed -n 's/^n//p'&lt;/code&gt; strips off. That's the quick answer above.&lt;/p&gt;

&lt;p&gt;For most dev servers this is the project folder, because &lt;code&gt;npm run dev&lt;/code&gt;, &lt;code&gt;pnpm dev&lt;/code&gt;, &lt;code&gt;rails s&lt;/code&gt; and &lt;code&gt;go run .&lt;/code&gt; all run where you typed them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: see the full command
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ps &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nb"&gt;command&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 48211
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This shows the whole command line, which often holds the path as well: &lt;code&gt;node /Users/you/Code/acme-web/node_modules/.bin/vite&lt;/code&gt;. It also tells two servers in the same folder apart, like &lt;code&gt;vite&lt;/code&gt; and &lt;code&gt;storybook&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To see when it started, which helps spot the one you left running yesterday:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ps &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;lstart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 48211
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: get the git branch
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nt"&gt;-C&lt;/span&gt; /Users/you/Code/acme-web branch &lt;span class="nt"&gt;--show-current&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-C&lt;/code&gt; runs git as if you were in that folder, so you don't have to &lt;code&gt;cd&lt;/code&gt;. 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.&lt;/p&gt;

&lt;p&gt;Two more that help here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;git -C &amp;lt;folder&amp;gt; rev-parse --show-toplevel&lt;/code&gt; gives the repo root when the server runs in a subfolder, like &lt;code&gt;packages/web&lt;/code&gt; in a monorepo.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;git -C &amp;lt;folder&amp;gt; worktree list&lt;/code&gt; shows every worktree of that repo and its branch.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Make it one command
&lt;/h2&gt;

&lt;p&gt;Add this to &lt;code&gt;~/.zshrc&lt;/code&gt;, open a new terminal, and run &lt;code&gt;whoport 3000&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;whoport&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;pid &lt;span class="nb"&gt;dir
  &lt;/span&gt;&lt;span class="nv"&gt;pid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;lsof &lt;span class="nt"&gt;-ti&lt;/span&gt; tcp:&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-sTCP&lt;/span&gt;:LISTEN | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="nv"&gt;$pid&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Nothing is listening on :&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;return &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="nb"&gt;dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;lsof &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pid&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; cwd &lt;span class="nt"&gt;-Fn&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'s/^n//p'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"PID     &lt;/span&gt;&lt;span class="nv"&gt;$pid&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Command &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;ps &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nb"&gt;command&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pid&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Folder  &lt;/span&gt;&lt;span class="nv"&gt;$dir&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$dir&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; rev-parse &lt;span class="nt"&gt;--is-inside-work-tree&lt;/span&gt; &amp;amp;&amp;gt;/dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Branch  &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$dir&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; branch &lt;span class="nt"&gt;--show-current&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;% whoport 3000
PID     48211
Command node /Users/you/Code/acme-web/node_modules/.bin/vite
Folder  /Users/you/Code/acme-web
Branch  feature/search
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To see every listening server with its folder at once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lsof &lt;span class="nt"&gt;-tiTCP&lt;/span&gt; &lt;span class="nt"&gt;-sTCP&lt;/span&gt;:LISTEN | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; | &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read &lt;/span&gt;pid&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%-6s %-18.18s %s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pid&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;ps &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nb"&gt;comm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pid&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s#.*/##'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;lsof &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pid&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; cwd &lt;span class="nt"&gt;-Fn&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'s/^n//p'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see a few apps of your own in there too, like your editor's helpers. Their folder is usually &lt;code&gt;/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you check this more than once a day: full disclosure, I built &lt;a href="https://portlivo.com/?utm_source=devto" rel="noopener noreferrer"&gt;Portlivo&lt;/a&gt;, 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.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Open it in your editor
&lt;/h2&gt;

&lt;p&gt;Once you have the folder, one command opens it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Editor&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Install the command&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;VS Code&lt;/td&gt;
&lt;td&gt;&lt;code&gt;code ~/Code/acme-web&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Command Palette → Shell Command: Install 'code' command in PATH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cursor&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cursor ~/Code/acme-web&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Command Palette → Shell Command: Install 'cursor' command&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zed&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zed ~/Code/acme-web&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Zed menu → Install CLI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Any app&lt;/td&gt;
&lt;td&gt;&lt;code&gt;open -a "Visual Studio Code" ~/Code/acme-web&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Nothing to install&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Put them together and you jump from a port straight to its code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;code &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;lsof &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;lsof &lt;span class="nt"&gt;-ti&lt;/span&gt; tcp:3000 &lt;span class="nt"&gt;-sTCP&lt;/span&gt;:LISTEN | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; cwd &lt;span class="nt"&gt;-Fn&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'s/^n//p'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  When the folder is / or makes no sense
&lt;/h2&gt;

&lt;p&gt;Some servers aren't started from a project folder. lsof is still right; the folder just isn't the useful answer.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you see&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;Where to look instead&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;com.docker.backend&lt;/code&gt; or OrbStack holds the port&lt;/td&gt;
&lt;td&gt;A container&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;docker ps --filter publish=3000&lt;/code&gt;, then the Compose label below&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Folder is &lt;code&gt;/&lt;/code&gt;, command is &lt;code&gt;postgres&lt;/code&gt;, &lt;code&gt;redis-server&lt;/code&gt; or &lt;code&gt;mysqld&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;A Homebrew service, started by launchd&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;brew services list&lt;/code&gt;. It isn't tied to a project.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Folder is &lt;code&gt;/&lt;/code&gt;, command is &lt;code&gt;nginx&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Laravel Herd or Valet&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;herd links&lt;/code&gt; or &lt;code&gt;valet links&lt;/code&gt; list the folder behind each &lt;code&gt;.test&lt;/code&gt; site&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Folder ends in &lt;code&gt;/public&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;php artisan serve&lt;/code&gt;, which runs from the public folder&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;git -C &amp;lt;folder&amp;gt; rev-parse --show-toplevel&lt;/code&gt; for the project root&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Folder is your home folder or &lt;code&gt;/&lt;/code&gt;, process is &lt;code&gt;node&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Started by pm2 or another process manager&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pm2 list&lt;/code&gt;, then &lt;code&gt;pm2 describe &amp;lt;name&amp;gt;&lt;/code&gt; shows its exec cwd&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lsof prints nothing, but the port is taken&lt;/td&gt;
&lt;td&gt;A process owned by root or another user&lt;/td&gt;
&lt;td&gt;Run the same commands with &lt;code&gt;sudo&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For a Docker Compose container, this prints the folder of the project that started it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker inspect &amp;lt;container&amp;gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;working_dir
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"com.docker.compose.project.working_dir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/Users/you/Code/acme-web"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the port is taken and you want it back, I wrote up &lt;a href="https://portlivo.com/guides/kill-process-on-port-mac?utm_source=devto" rel="noopener noreferrer"&gt;how to stop the process on a port safely&lt;/a&gt;, including the ones that come straight back.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is there a pwdx command on Mac?&lt;/strong&gt;&lt;br&gt;
No. &lt;code&gt;pwdx&lt;/code&gt; and &lt;code&gt;/proc/&amp;lt;PID&amp;gt;/cwd&lt;/code&gt; are Linux only. On macOS, &lt;code&gt;lsof -a -p &amp;lt;PID&amp;gt; -d cwd&lt;/code&gt; does the same job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I find which port a project is using?&lt;/strong&gt;&lt;br&gt;
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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does this work for Python, Ruby, Go and PHP servers?&lt;/strong&gt;&lt;br&gt;
Yes. lsof and ps don't care what language the process is in. I tested the steps above with &lt;code&gt;python3 -m http.server&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does lsof show nothing for a port I know is in use?&lt;/strong&gt;&lt;br&gt;
Usually the process belongs to another user, so try &lt;code&gt;sudo&lt;/code&gt;. If it's a Docker container, lsof shows Docker's own process, not the app inside it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I see this without the terminal?&lt;/strong&gt;&lt;br&gt;
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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;lsof finds the PID, &lt;code&gt;lsof -d cwd&lt;/code&gt; turns it into a folder, and &lt;code&gt;git -C&lt;/code&gt; gives you the branch. Put them in a &lt;code&gt;whoport&lt;/code&gt; function and "which project is this?" takes one command.&lt;/p&gt;

&lt;p&gt;How many dev servers do you usually have running at once?&lt;/p&gt;

&lt;p&gt;I'm Anmol. I build Mac apps and write about Mac productivity. Follow me on X at &lt;a href="https://x.com/anmolsethi_dev" rel="noopener noreferrer"&gt;@anmolsethi_dev&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>macos</category>
      <category>webdev</category>
      <category>terminal</category>
      <category>productivity</category>
    </item>
    <item>
      <title>What's running on my Mac? How to see every dev server and port</title>
      <dc:creator>Anmol Sethi</dc:creator>
      <pubDate>Mon, 19 Oct 2026 02:24:46 +0000</pubDate>
      <link>https://dev.to/anmolsethi_dev/whats-running-on-my-mac-how-to-see-every-dev-server-and-port-56lh</link>
      <guid>https://dev.to/anmolsethi_dev/whats-running-on-my-mac-how-to-see-every-dev-server-and-port-56lh</guid>
      <description>&lt;p&gt;A Vite app from this morning, an &lt;code&gt;artisan serve&lt;/code&gt; from yesterday, Postgres, Redis, and whatever your coding agent started in the background. By the afternoon most of us have no idea what's actually running. Here's how to see all of it from Terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one command
&lt;/h2&gt;

&lt;p&gt;This lists every TCP port something is listening on, with the program and its PID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lsof &lt;span class="nt"&gt;-nP&lt;/span&gt; &lt;span class="nt"&gt;-iTCP&lt;/span&gt; &lt;span class="nt"&gt;-sTCP&lt;/span&gt;:LISTEN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A typical afternoon looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;COMMAND     PID USER   FD   TYPE  DEVICE SIZE/OFF NODE NAME
node      48213 you    23u  IPv6  0x8c2f…      0t0  TCP *:3000 (LISTEN)
node      51877 you    24u  IPv4  0x1d07…      0t0  TCP 127.0.0.1:5173 (LISTEN)
php       52210 you     8u  IPv4  0x6b3a…      0t0  TCP 127.0.0.1:8000 (LISTEN)
postgres    611 you     7u  IPv6  0x4e19…      0t0  TCP [::1]:5432 (LISTEN)
redis-ser   702 you     6u  IPv4  0x93c4…      0t0  TCP 127.0.0.1:6379 (LISTEN)
ControlCe   423 you     9u  IPv4  0x2f88…      0t0  TCP *:7000 (LISTEN)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things worth knowing when you read it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;*:3000&lt;/code&gt; accepts connections from other devices on your network. &lt;code&gt;127.0.0.1&lt;/code&gt; or &lt;code&gt;[::1]&lt;/code&gt; means only this Mac.&lt;/li&gt;
&lt;li&gt;Names are cut to nine characters: &lt;code&gt;redis-ser&lt;/code&gt; is redis-server, and &lt;code&gt;ControlCe&lt;/code&gt; is ControlCenter, which is macOS's AirPlay Receiver (it's why port 5000 is often taken). Add &lt;code&gt;+c 0&lt;/code&gt; for full names.&lt;/li&gt;
&lt;li&gt;One server can appear twice, once for IPv4 and once for IPv6, with the same PID.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Which project is it?
&lt;/h2&gt;

&lt;p&gt;lsof says &lt;code&gt;node&lt;/code&gt; or &lt;code&gt;php&lt;/code&gt;, not "the shop frontend". To get the full command and the folder a process runs in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ps &lt;span class="nt"&gt;-p&lt;/span&gt; &amp;lt;PID&amp;gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nb"&gt;command&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
lsof &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &amp;lt;PID&amp;gt; &lt;span class="nt"&gt;-d&lt;/span&gt; cwd &lt;span class="nt"&gt;-Fn&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'s/^n//p'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or for every listening process at once, folder first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;pid &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;lsof &lt;span class="nt"&gt;-ti&lt;/span&gt; &lt;span class="nt"&gt;-iTCP&lt;/span&gt; &lt;span class="nt"&gt;-sTCP&lt;/span&gt;:LISTEN | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\t%s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;lsof &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pid&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; cwd &lt;span class="nt"&gt;-Fn&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'s/^n//p'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;ps &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pid&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nb"&gt;command&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A folder of &lt;code&gt;/&lt;/code&gt; usually means a system or app process, like an editor helper, rather than one of your projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which ones can your phone reach?
&lt;/h2&gt;

&lt;p&gt;Anything not bound to &lt;code&gt;127.0.0.1&lt;/code&gt; or &lt;code&gt;[::1]&lt;/code&gt; is reachable from your network:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lsof &lt;span class="nt"&gt;-nP&lt;/span&gt; &lt;span class="nt"&gt;-iTCP&lt;/span&gt; &lt;span class="nt"&gt;-sTCP&lt;/span&gt;:LISTEN | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-vE&lt;/span&gt; &lt;span class="s1"&gt;'127\.0\.0\.1|\[::1\]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's what you want when testing on a phone, and worth checking on café Wi-Fi. Most dev servers only listen on localhost unless you pass &lt;code&gt;--host&lt;/code&gt; (Vite) or &lt;code&gt;-H 0.0.0.0&lt;/code&gt; (Next.js).&lt;/p&gt;

&lt;h2&gt;
  
  
  Docker and Homebrew hide behind other names
&lt;/h2&gt;

&lt;p&gt;Ports published by Docker show up in lsof as &lt;code&gt;com.docker.backend&lt;/code&gt; (or OrbStack), not as your app. Homebrew services show as the database itself. List them directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker ps &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{.Names}}\t{{.Ports}}'&lt;/span&gt;
brew services list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why not Activity Monitor?
&lt;/h2&gt;

&lt;p&gt;Activity Monitor shows processes with CPU and memory, but not their ports. You can double-click a process and open Open Files and Ports, one process at a time. Fine for one server, slow for ten.&lt;/p&gt;

&lt;h2&gt;
  
  
  Or keep it in the menu bar
&lt;/h2&gt;

&lt;p&gt;I ran these commands often enough that I built &lt;a href="https://portlivo.com/?utm_source=devto" rel="noopener noreferrer"&gt;Portlivo&lt;/a&gt;, a Mac menu bar app that keeps this list live: every server named by its project, with the command, uptime, whether it's answering, CPU and memory, and Docker and Homebrew services alongside. One click opens, restarts, tails or stops any of them. It's a native app with a 7-day free trial.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;lsof -nP -iTCP -sTCP:LISTEN&lt;/code&gt; shows every listening port.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ps -p &amp;lt;PID&amp;gt; -o command=&lt;/code&gt; and the cwd lookup tell you which project it is.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;*&lt;/code&gt; in the address means your network can reach it; &lt;code&gt;127.0.0.1&lt;/code&gt; means only you.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>programming</category>
      <category>webdev</category>
      <category>terminal</category>
      <category>cli</category>
    </item>
    <item>
      <title>How to fix "port already in use" on a Mac (and stop it happening)</title>
      <dc:creator>Anmol Sethi</dc:creator>
      <pubDate>Sun, 18 Oct 2026 10:30:00 +0000</pubDate>
      <link>https://dev.to/anmolsethi_dev/how-to-fix-port-already-in-use-on-a-mac-and-stop-it-happening-4fa8</link>
      <guid>https://dev.to/anmolsethi_dev/how-to-fix-port-already-in-use-on-a-mac-and-stop-it-happening-4fa8</guid>
      <description>&lt;p&gt;You run &lt;code&gt;npm run dev&lt;/code&gt; and get this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: listen EADDRINUSE: address already in use :::3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Something is already listening on port 3000. Usually it's a dev server you started earlier and forgot about: a terminal tab you closed without stopping it, a project from yesterday, or something a coding agent started in the background.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix it in two commands
&lt;/h2&gt;

&lt;p&gt;Find what's holding the port:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lsof &lt;span class="nt"&gt;-i&lt;/span&gt; :3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output lists the process name and its PID (process ID):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;COMMAND   PID   USER   FD   TYPE  DEVICE  SIZE/OFF NODE NAME
node    41234  you    23u  IPv6  0x...       0t0  TCP *:hbci (LISTEN)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then stop it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;kill &lt;/span&gt;41234
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it won't go, &lt;code&gt;kill -9 41234&lt;/code&gt; forces it. Use that as a last resort: the process gets no chance to clean up.&lt;/p&gt;

&lt;p&gt;One-liner, if you're sure whatever is on the port can go:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lsof &lt;span class="nt"&gt;-ti&lt;/span&gt; :3000 | xargs &lt;span class="nb"&gt;kill&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why it keeps happening
&lt;/h2&gt;

&lt;p&gt;A modern project rarely runs one server. A frontend on 5173, an API on 8000, Postgres on 5432, Redis on 6379, maybe a worker and a docs site. Multiply that by the three projects you touched this week, and nothing on macOS shows you the full picture. Activity Monitor lists processes, not ports. &lt;code&gt;lsof&lt;/code&gt; answers one port at a time, and only when you already know which one to ask about.&lt;/p&gt;

&lt;p&gt;So stale servers pile up quietly, and you only notice when one blocks the port you need.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built instead
&lt;/h2&gt;

&lt;p&gt;I got tired of the &lt;code&gt;lsof&lt;/code&gt; ritual, so I built &lt;a href="https://portlivo.com/?utm_source=devto" rel="noopener noreferrer"&gt;Portlivo&lt;/a&gt;, a Mac menu bar app that shows every local dev server and port at once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which project each server belongs to, and the command that started it&lt;/li&gt;
&lt;li&gt;whether it's actually answering, plus CPU and memory&lt;/li&gt;
&lt;li&gt;one click to open it in the browser, restart it, tail its logs or kill it&lt;/li&gt;
&lt;li&gt;Homebrew services like Postgres and Redis in the same list&lt;/li&gt;
&lt;li&gt;a QR code to open any server on your phone&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a port clash happens, it notifies you with a &lt;strong&gt;Kill holder &amp;amp; retry&lt;/strong&gt; button, which is the button I'd wanted for years. There's also a command palette, keyboard shortcuts and a &lt;code&gt;devports&lt;/code&gt; command-line tool if you'd rather stay in the terminal.&lt;/p&gt;

&lt;p&gt;It's a native SwiftUI app for macOS 14 or later, on Apple silicon and Intel. No account, no subscription: a 7-day free trial, then a one-time licence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;lsof -i :&amp;lt;port&amp;gt;&lt;/code&gt; shows what's on a port; &lt;code&gt;kill &amp;lt;pid&amp;gt;&lt;/code&gt; stops it.&lt;/li&gt;
&lt;li&gt;Prefer &lt;code&gt;kill&lt;/code&gt; over &lt;code&gt;kill -9&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The port clash is a symptom. The real problem is not being able to see what's running.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you'd like to see everything at a glance, &lt;a href="https://portlivo.com/?utm_source=medium" rel="noopener noreferrer"&gt;Portlivo&lt;/a&gt; is free to try for 7 days.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>programming</category>
      <category>devtool</category>
    </item>
  </channel>
</rss>
