---
title: My laptop is closed and the agents are still working
description: "A Mac mini that never sleeps, tmux, and a private network, so coding agents keep working after I shut the lid. The actual setup, the question tmux could not answer, and the tool I built to answer it."
date: 2026-08-05
tags: [ai, agents, build-in-public, tools]
url: "https://mahdif.com/blog/my-laptop-is-closed-and-the-agents-are-still-working/"
---

# My laptop is closed and the agents are still working

A Mac mini that never sleeps, tmux, and a private network, so coding agents keep working after I shut the lid. The actual setup, the question tmux could not answer, and the tool I built to answer it.

There is a Mac mini in my house that has not been switched off in months. It is not a server in a rack somewhere. It sits on a desk, it runs around the clock, and right now there are coding agents working inside it.

My laptop is closed. I am not in the room.

This post is how that actually works. Not a productivity system, and not the influencer version where a diagram does the work. A machine that stays on, a terminal multiplexer, a private network, and the one thing I got wrong for long enough that I ended up building a tool about it.

<figure style="margin:2.75rem 0;">
<div style="border:1px solid var(--border);border-radius:var(--radius-lg);overflow:hidden;background:var(--card);">
<svg viewBox="0 0 1200 630" xmlns="http://www.w3.org/2000/svg" style="display:block;width:100%;height:auto" role="img" aria-label="A diagram of the setup. On the left, a Mac mini that never sleeps, holding four tmux sessions named awake, muxo, site and brain, each with a status dot. On the right, two clients of that same machine: a closed laptop connected by a dashed line labelled detached, and a phone connected by a solid line labelled attached, over Tailscale. The work runs on the mini regardless of which client is connected.">
<defs><linearGradient id="lc-grad" x1="0" y1="0" x2="1200" y2="630" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#6366F1"/><stop offset="0.5" stop-color="#A855F7"/><stop offset="1" stop-color="#EC4899"/></linearGradient></defs>
<style>.lc-bg{fill:var(--card)}.lc-panel{fill:var(--background);stroke:var(--border);stroke-width:1}.lc-line{stroke:var(--border);stroke-width:1}.lc-eye{fill:var(--muted-foreground);font-family:var(--font-mono);font-size:19px;letter-spacing:4px}.lc-h{fill:var(--foreground);font-family:var(--font-mono);font-size:24px}.lc-sub{fill:var(--muted-foreground);font-family:var(--font-sans);font-size:17px}.lc-row{fill:var(--foreground);font-family:var(--font-mono);font-size:20px}.lc-cap{fill:var(--muted-foreground);font-family:var(--font-mono);font-size:15px;letter-spacing:2px}.lc-dev{fill:none;stroke:var(--muted-foreground);stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round}</style>
<rect class="lc-bg" width="1200" height="630"/>
<text class="lc-eye" x="90" y="76">ONE MACHINE, MANY CLIENTS</text>
<rect class="lc-panel" x="80" y="130" width="400" height="410" rx="18"/>
<g class="lc-dev"><rect x="118" y="168" width="52" height="20" rx="7"/><circle cx="158" cy="178" r="1.6" fill="var(--muted-foreground)" stroke="none"/></g>
<text class="lc-h" x="192" y="186">Mac mini</text>
<text class="lc-sub" x="118" y="222">never sleeps, never logs out</text>
<line class="lc-line" x1="80" y1="248" x2="480" y2="248"/>
<circle cx="124" cy="296" r="7" fill="var(--muted-foreground)"/><text class="lc-row" x="150" y="303">awake</text>
<circle cx="124" cy="352" r="7" fill="#F59E0B"/><text class="lc-row" x="150" y="359">muxo</text>
<circle cx="124" cy="408" r="7" fill="#10B981"/><text class="lc-row" x="150" y="415">site</text>
<circle cx="124" cy="464" r="6.5" fill="none" stroke="var(--muted-foreground)" stroke-width="2"/><text class="lc-row" x="150" y="471">brain</text>
<text class="lc-cap" x="118" y="518">4 TMUX SESSIONS</text>
<path d="M480,290 C600,290 660,220 790,220" fill="none" stroke="var(--border)" stroke-width="2.5" stroke-dasharray="7 8"/>
<rect x="583" y="240" width="104" height="30" rx="8" fill="var(--card)"/>
<text class="lc-cap" x="635" y="261" text-anchor="middle">DETACHED</text>
<path d="M480,390 C600,390 660,410 790,410" fill="none" stroke="url(#lc-grad)" stroke-width="2.5"/>
<rect x="583" y="358" width="104" height="30" rx="8" fill="var(--card)"/>
<text class="lc-cap" x="635" y="379" text-anchor="middle">ATTACHED</text>
<g fill="var(--muted-foreground)"><g opacity="0.3"><circle cx="546" cy="425" r="4.5"/><circle cx="559" cy="425" r="4.5"/><circle cx="572" cy="425" r="4.5"/><circle cx="546" cy="438" r="4.5"/><circle cx="572" cy="438" r="4.5"/></g><circle cx="559" cy="438" r="4.5"/><circle cx="546" cy="451" r="4.5"/><circle cx="559" cy="451" r="4.5"/><circle cx="572" cy="451" r="4.5"/></g>
<text class="lc-cap" x="592" y="448">OVER TAILSCALE</text>
<rect class="lc-panel" x="790" y="150" width="330" height="140" rx="14"/>
<g class="lc-dev"><rect x="824" y="186" width="46" height="30" rx="4"/><path d="M814,226 h66"/></g>
<text class="lc-h" x="906" y="205">laptop</text>
<text class="lc-sub" x="906" y="235">closed, in a bag</text>
<rect class="lc-panel" x="790" y="340" width="330" height="140" rx="14"/>
<g class="lc-dev"><rect x="832" y="372" width="28" height="48" rx="7"/><path d="M841,380 h10"/></g>
<text class="lc-h" x="906" y="395">phone</text>
<text class="lc-sub" x="906" y="425">in my pocket</text>
</svg>
</div>
<figcaption style="margin-top:0.85rem;text-align:center;font-size:0.85rem;color:var(--muted-foreground);">The work lives on the mini. Every client is optional.</figcaption>
</figure>

## tmux is the entire trick

A terminal session normally dies with the window that holds it. Close the lid, kill the process. That single fact is what keeps most people tethered to their laptop.

tmux breaks it. Sessions run in a separate process that does not care whether anyone is attached to them. Start Claude Code inside a tmux session and it keeps going after you detach, after you close the terminal, after you shut the laptop and drive somewhere.

Detaching is not stopping. Once that lands, the machine stops being something you sit at and becomes something you check on.

## Tailscale makes the machine reachable without making it public

The mini is on a private network shared by my devices. From my phone, anywhere, it behaves as if we are on the same wifi. Nothing is exposed to the internet, and there is no port forwarding involved.

So from my phone, anywhere, I can reach a session that has been running for hours, see what the agent did, and unblock it. When I need a real terminal on the phone I use [Termius](https://termius.com/), the best terminal client I have found on iOS. But the terminal turned out to be the fallback, not the main event. More on that in a minute.

<figure style="margin:2.75rem 0;">
<img src="/images/blog/my-laptop-is-closed-and-the-agents-are-still-working/termius-phone.png" alt="A phone in a minimal dark frame running Termius, connected to the Mac mini over SSH. The tmux session named brain is attached, showing Claude Code idle at its prompt inside the Second Brain directory, with Termius's host chip reading Mac mini and its terminal key bar and keyboard below." width="908" height="1782" style="display:block;width:100%;max-width:25rem;height:auto;margin:0 auto;" />
<figcaption style="margin-top:0.85rem;text-align:center;font-size:0.85rem;color:var(--muted-foreground);">A real terminal, on the phone, attached to the machine at home.</figcaption>
</figure>

The work happens on hardware I own, in my house, and I am just a client that comes and goes.

## The part I kept getting wrong

I assumed my problem was tmux commands. It was not. I did google the same three commands more often than I would like to admit, but that was an annoyance, not the thing costing me real time.

The thing costing me real time was a question tmux cannot answer. Which of these sessions is stuck waiting on me?

tmux will happily list every session. It will not tell you that one agent finished twenty minutes ago, another is mid turn, and a third has been sitting on a permission prompt since lunch. So I was attaching to each session in turn to find out, which is the slowest possible way to answer a question I had twenty times a day.

<figure style="margin:2.75rem 0;">
<div style="border:1px solid var(--border);border-radius:var(--radius-lg);overflow:hidden;background:var(--card);">
<svg viewBox="0 0 1200 520" xmlns="http://www.w3.org/2000/svg" style="display:block;width:100%;height:auto" role="img" aria-label="A terminal window running tmux ls. The output lists four sessions, awake, brain, muxo and site, each showing only a window count and a creation date. Below it, tmux attach, with a note that attaching to each one in turn is the only way to find out what an agent is doing.">
<defs><linearGradient id="tl-grad" x1="0" y1="0" x2="1200" y2="520" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#6366F1"/><stop offset="0.5" stop-color="#A855F7"/><stop offset="1" stop-color="#EC4899"/></linearGradient></defs>
<style>.tl-bg{fill:var(--card)}.tl-win{fill:var(--background);stroke:var(--border);stroke-width:1}.tl-line{stroke:var(--border);stroke-width:1}.tl-title{fill:var(--muted-foreground);font-family:var(--font-mono);font-size:16px}.tl-code{font-family:var(--font-mono);font-size:19px;fill:var(--foreground)}.tl-dim{font-family:var(--font-mono);font-size:19px;fill:var(--muted-foreground)}.tl-eye{fill:var(--muted-foreground);font-family:var(--font-mono);font-size:19px;letter-spacing:4px}</style>
<rect class="tl-bg" width="1200" height="520"/>
<text class="tl-eye" x="110" y="66">WHICH ONE IS STUCK?</text>
<rect class="tl-win" x="110" y="96" width="980" height="370" rx="16"/>
<circle cx="146" cy="130" r="6.5" fill="var(--border)"/><circle cx="170" cy="130" r="6.5" fill="var(--border)"/><circle cx="194" cy="130" r="6.5" fill="var(--border)"/>
<text class="tl-title" x="600" y="136" text-anchor="middle">ssh mini</text>
<line class="tl-line" x1="110" y1="162" x2="1090" y2="162"/>
<text class="tl-code" x="150" y="208"><tspan fill="url(#tl-grad)">$</tspan> tmux ls</text>
<text class="tl-dim" x="150" y="248">awake: 1 windows (created Sun Jun 14 09:12)</text>
<text class="tl-dim" x="150" y="288">brain: 1 windows (created Fri Aug  1 18:03)</text>
<text class="tl-dim" x="150" y="328">muxo: 2 windows (created Tue Aug  4 07:41)</text>
<text class="tl-dim" x="150" y="368">site: 1 windows (created Tue Aug  4 07:52)</text>
<text class="tl-code" x="150" y="428"><tspan fill="url(#tl-grad)">$</tspan> tmux attach -t muxo   <tspan class="tl-dim"># attach, look, detach, repeat</tspan></text>
</svg>
</div>
<figcaption style="margin-top:0.85rem;text-align:center;font-size:0.85rem;color:var(--muted-foreground);">tmux knows what exists. It has no idea who needs you.</figcaption>
</figure>

## So I built a dashboard for it

It is called muxo. One list of every session on the machine, sorted so whatever wants me is at the top.

<figure style="margin:2.75rem 0;">
<video autoplay muted loop playsinline controls preload="metadata" poster="/images/blog/my-laptop-is-closed-and-the-agents-are-still-working/videos/muxo-demo-poster.jpg" style="display:block;width:100%;border:1px solid var(--border);border-radius:var(--radius-lg);background:var(--card);">
  <source src="/images/blog/my-laptop-is-closed-and-the-agents-are-still-working/videos/muxo-demo.mp4" type="video/mp4">
  A close look at muxo: the session list with its status dots, and the mirror opening on an agent waiting for approval.
</video>
<figcaption style="margin-top:0.85rem;text-align:center;font-size:0.85rem;color:var(--muted-foreground);">Every session on the machine, and what each one is doing, without attaching to any of them.</figcaption>
</figure>

The whole interface is one question answered by colour.

<figure style="margin:2.75rem 0;">
<div style="border:1px solid var(--border);border-radius:var(--radius-lg);overflow:hidden;background:var(--card);">
<svg viewBox="0 0 1200 560" xmlns="http://www.w3.org/2000/svg" style="display:block;width:100%;height:auto" role="img" aria-label="The four session states in muxo. A filled amber dot means an agent is blocked on a prompt and wants you now. A filled green dot means the agent is mid turn and working. A faint green dot means the agent is idle at its prompt and finished. A hollow outline means the session is just a shell with no agent in it.">
<style>.st-bg{fill:var(--card)}.st-line{stroke:var(--border);stroke-width:1}.st-eye{fill:var(--muted-foreground);font-family:var(--font-mono);font-size:19px;letter-spacing:4px}.st-name{fill:var(--foreground);font-family:var(--font-mono);font-size:26px}.st-desc{fill:var(--muted-foreground);font-family:var(--font-sans);font-size:20px}</style>
<rect class="st-bg" width="1200" height="560"/>
<text class="st-eye" x="90" y="80">SORTED SO WHATEVER WANTS ME IS AT THE TOP</text>
<line class="st-line" x1="90" y1="126" x2="1110" y2="126"/>
<circle cx="118" cy="186" r="11" fill="#F59E0B"/><text class="st-name" x="160" y="195">amber</text><text class="st-desc" x="380" y="194">blocked on a prompt, wants me now</text>
<line class="st-line" x1="90" y1="230" x2="1110" y2="230"/>
<circle cx="118" cy="290" r="11" fill="#10B981"/><text class="st-name" x="160" y="299">green</text><text class="st-desc" x="380" y="298">mid turn, working</text>
<line class="st-line" x1="90" y1="334" x2="1110" y2="334"/>
<circle cx="118" cy="394" r="11" fill="#10B981" fill-opacity="0.35"/><text class="st-name" x="160" y="403">faint green</text><text class="st-desc" x="380" y="402">idle at its prompt, done</text>
<line class="st-line" x1="90" y1="438" x2="1110" y2="438"/>
<circle cx="118" cy="498" r="10" fill="none" stroke="var(--muted-foreground)" stroke-width="2"/><text class="st-name" x="160" y="507">outline</text><text class="st-desc" x="380" y="506">just a shell, no agent in it</text>
</svg>
</div>
<figcaption style="margin-top:0.85rem;text-align:center;font-size:0.85rem;color:var(--muted-foreground);">Four states. I read the sidebar in about a second.</figcaption>
</figure>

That is the whole product. Not a chat renderer, because claude.ai already does that well. Not a full terminal, because my SSH client already does that well. It answers one question and hands off for everything else.

My favourite detail is the part that decides whether an agent is running in a pane. You cannot look for the word "claude" in the process name, because Claude Code's process name is its version number. So the check is a regular expression for digits and dots. That is not a thing you can read in a blog post. You only find it by building the thing and watching it fail.

There is no database either. tmux already knows what is running, so muxo asks it on every refresh rather than keeping its own copy. A second source of truth can only drift from the first one.

## One tap moves it to a better screen

The mirror is for a quick look, and on the phone the key bar under it clears most blocks in a tap or two, because a stuck agent is usually sitting on a permission prompt or a menu. What the mirror is not for is reading a long conversation or typing a real reply, and I do not pretend otherwise.

muxo has a toggle for that. It sends `/remote-control` to the Claude Code session, and the same session becomes available in the Claude iOS app on my phone and in the web UI on claude.ai. I pick up exactly where the agent and I left off, in apps that are better at the reading half than anything I would build. Proper formatting, real image previews, and a browser the agent can actually use.

<figure style="margin:2.75rem 0;">
<img src="/images/blog/my-laptop-is-closed-and-the-agents-are-still-working/rc-toggle.jpg" alt="A close crop of the bottom of a muxo session mirror. A row of key chips reads esc, arrows, space, enter, shift tab, control C, and last a chip labelled RC with a remote control icon, highlighted with a violet ring. Below it sits the send box." width="1440" height="310" style="display:block;width:100%;height:auto;" />
<figcaption style="margin-top:0.85rem;text-align:center;font-size:0.85rem;color:var(--muted-foreground);">The RC chip. One tap sends /remote-control, and the session shows up in the Claude app.</figcaption>
</figure>

I will admit this is not the setup I designed. I thought the terminal in my pocket would be the workhorse and the Claude apps the occasional convenience. It went the other way. Even with Termius right there, when I need to make real changes on the go I reach for the Claude app, because continuing a conversation is the thing it is built for. muxo's real job turned out to be telling me who needs me and flipping that switch.

<figure style="margin:2.75rem 0;">
<img src="/images/blog/my-laptop-is-closed-and-the-agents-are-still-working/claude-app-phone.png" alt="A phone in a minimal dark frame showing the Claude iOS app. The open conversation is the muxo session from the Mac mini, picked up over remote control, with a reply from the agent on screen and the message box reading Add feedback." width="908" height="1782" style="display:block;width:100%;max-width:25rem;height:auto;margin:0 auto;" />
<figcaption style="margin-top:0.85rem;text-align:center;font-size:0.85rem;color:var(--muted-foreground);">The same muxo session, picked up in the Claude iOS app. This is where changes on the go actually happen.</figcaption>
</figure>

A tool that knows what it is bad at ends up more useful than one that pretends.

## The session that keeps the machine awake

There is a session called `awake` whose only job is to run `caffeinate` so the mini never sleeps. If it dies, the machine sleeps, and every agent on it stops.

muxo refuses to offer that session for deletion. It sits in the sidebar marked as infrastructure.

I did not plan that feature. I built it after looking at a list of sessions and realising that one of them was load bearing and looked exactly like the others.

## Where the human stays

The honest part of this setup is the security posture, so let me be plain about it. The box that lets me answer a prompt from my phone types into a real terminal on a real machine. That is remote command execution, on purpose, because that is the entire point. It binds to localhost by default and refuses to start on any reachable address unless you have set a token. The right way to think about exposing it is the way you think about exposing SSH.

The other honest part is what this does not fix. Running agents around the clock does not make them better. It means I find out sooner. An agent that went wrong at two in the afternoon is still wrong at six. The difference is that it turns amber and I stop it, instead of discovering it at bedtime with four hours of confident nonsense behind it.

The judgement did not get automated. It just got faster to apply.

## If you want to try this

Start with tmux and nothing else. Run one long task inside a session, close the lid, come back later and attach. That single experiment is the whole idea, and it costs you an afternoon at most.

Put the machine on a private network before you put anything interesting on it. Do that part first, not after.

Use a real terminal client on your phone rather than fighting a web one. The phone is not where you do the work. It is where you unblock the work.

And do not build the dashboard first. I used this setup for a while before I built anything, which is the only reason muxo answers the question I actually had instead of the one I would have guessed at.

The usual pitch for agents is autonomy, as if the goal were a machine that no longer needs you. That is not what I got. What I got is smaller and more useful. The work no longer stops when I do.
