All posts
Lid-Safe Mode: Đừng Ôm MacBook Đi Khắp Nhà Chỉ Vì Muốn Agent Tiếp Tục Chạy
macosclaudecodepmsetpowermanagementsudoersaiagentremotecontrolautomationreverseengineeringdevtools

Lid-Safe Mode: Đừng Ôm MacBook Đi Khắp Nhà Chỉ Vì Muốn Agent Tiếp Tục Chạy

Tuan Phan V. Q.'s avatarTuan Phan V. Q.
Table of Contents14 sections

Kể từ khi Claude CodeRemote ControlDispatch, mình bắt đầu để ý đến một cảnh khá buồn cười. Không chỉ mình mà rất nhiều người cũng vậy: ôm nguyên chiếc MacBook đang mở nắp đi khắp nhà như bưng một cái khay. Đi lấy nước thì mang theo, ngồi xuống sofa cũng mang theo, đứng trong bếp vẫn phải giữ nắp mở. Không phải vì thích, mà vì chỉ cần gập màn hình xuống là macOS cho máy ngủ, mọi thứ đang chạy cũng dừng theo.

Mình thì chọn một cách khác.

Chiếc MacBook giờ nằm yên trong ngăn kéo. Nắp đóng. Màn hình tắt. Quạt gần như không nghe thấy gì. Mình vẫn điều khiển nó từ điện thoại như bình thường, gửi lệnh cho Claude Code và để nó tiếp tục làm việc trên đúng môi trường phát triển của mình. Để làm được điều đó chỉ cần vài dòng cấu hình, nhưng hành trình đi đến đáp án lại thú vị hơn mình tưởng rất nhiều, bởi vì giải pháp đầu tiên mình tin chắc sẽ đúng hóa ra lại hoàn toàn sai.

Vấn đề

Điểm hay nhất của Remote ControlDispatch là mọi thứ đều chạy ngay trên máy của bạn. Repository vẫn nằm trên ổ cứng của bạn, MCP server vẫn ở đó, shell vẫn là shell quen thuộc và mọi công cụ trong workflow vẫn hoạt động. Bạn có thể khởi động một tác vụ từ điện thoại, rời khỏi bàn làm việc và để chiếc Mac ở nhà tiếp tục xử lý trên đúng project đang mở.

Cho đến khi bạn đóng nắp máy.

Ngay khoảnh khắc đó, macOS đưa máy vào chế độ ngủ. CPU ngừng xử lý, kết nối mạng biến mất và phiên làm việc cũng kết thúc.

AnthropicCloud Sessions để giải quyết một phần câu chuyện này. Với nhiều trường hợp, đó là một lựa chọn hợp lý. Tuy nhiên, đây lại là một hướng tiếp cận khác. Bạn không còn chạy trên môi trường cục bộ, không còn MCP server của riêng mình và cũng không còn những công cụ "cây nhà lá vườn" mà project phụ thuộc. Cloud Sessions giải quyết bài toán chạy ở nơi khác, chứ không giải quyết bài toán tiếp tục chạy trên chính chiếc Mac của bạn khi nắp đã đóng.

Vậy nếu vẫn muốn mọi thứ chạy cục bộ, làm sao để MacBook tiếp tục thức khi nắp đã gập xuống mà không phải biến nó thành món đồ lúc nào cũng phải mở hé rồi mang theo khắp nhà?

Vì sao caffeinate không đủ?

Nếu từng tìm cách giải quyết chuyện này, khả năng cao bạn cũng sẽ nghĩ giống mình: chỉ cần chạy caffeinate là xong. Nghe hoàn toàn hợp lý.

Thực tế, caffeinate hoạt động bằng cách tạo ra các power management assertion như PreventUserIdleSystemSleep hay PreventUserIdleDisplaySleep. Đây là cơ chế mà ứng dụng dùng để báo với powerd rằng hệ thống vẫn đang bận làm việc và không nên chuyển sang trạng thái ngủ chỉ vì người dùng không tương tác.

Điểm quan trọng là đóng nắp máy không phải idle sleep.

Khi nắp được gập xuống, macOS đi qua một nhánh xử lý hoàn toàn khác. Thay vì kiểm tra xem có tiến trình nào đang giữ assertion hay không, hệ điều hành sẽ quyết định xem chế độ clamshell có được phép hoạt động. Có màn hình ngoài không? Có bàn phím hoặc chuột ngoài không? Nếu không, đặc biệt khi đang chạy bằng pin, máy sẽ ngủ bất kể đang có bao nhiêu assertion được giữ.

Đó là lý do caffeinate gần như không có tác dụng trong trường hợp này. Nó không hề bị lỗi, chỉ đơn giản là nó được thiết kế để giải quyết một bài toán khác.

Nói ngắn gọn, mọi người ôm chiếc MacBook mở nắp đi khắp nhà không phải vì caffeinate hoạt động kém, mà vì nó chưa bao giờ được tạo ra để xử lý clamshell sleep.

Cách mình giải quyết

May mắn là macOS vẫn có một cách override hành vi này, và nó chỉ gói gọn trong một lệnh:

sudo pmset -a disablesleep 1

Khác với caffeinate, đây không phải là một yêu cầu gửi đến powerd. disablesleep thay đổi trực tiếp hành vi quản lý nguồn của hệ thống, buộc macOS không đưa máy vào trạng thái ngủ chỉ vì nắp đã đóng. Điều đó có nghĩa là nó hoạt động khi dùng pin, khi cắm sạc và cả khi không có bất kỳ màn hình hay thiết bị ngoại vi nào được kết nối.

Khi không còn cần nữa, chỉ việc bật lại hành vi mặc định:

sudo pmset -a disablesleep 0

Trước khi xây dựng bất cứ thứ gì xung quanh cơ chế này, mình muốn chắc rằng nó vẫn còn hoạt động trên macOS Tahoe. Khá nhiều bài viết trên Internet đã vài năm tuổi và pmset cũng thay đổi không ít qua từng phiên bản. Vì vậy mình kiểm tra lại toàn bộ bằng pmset -g, xác nhận rằng SleepDisabled thực sự chuyển từ 0 sang 1 rồi quay trở lại như mong đợi.

Sau khi biết nền tảng đủ tin cậy, mình bắt đầu nghĩ đến việc biến nó thành một workflow đủ tiện để dùng hằng ngày.

Xây dựng một workflow nhỏ

Mục tiêu của mình không phải viết thêm một ứng dụng mới hay chạy thêm một daemon nền chỉ để quản lý chuyện đóng mở nắp máy. Mình chỉ cần một cơ chế đủ đơn giản để bật, đủ rõ ràng để biết nó đang bật và đủ an toàn để tắt lại sau khi dùng xong.

Nếu bạn cũng dùng Claude Code và muốn dựng y hệt trên máy mình, dưới đây là toàn bộ các bước.

Bước 1 — Tạo hai slash command

Tạo file ~/.claude/commands/lidsafe.md:

---
description: Arm lid-safe mode
---

1. Run: `sudo /usr/bin/pmset -a disablesleep 1`
2. Verify: `pmset -g | grep SleepDisabled` — should read `SleepDisabled 1`
3. Report back: "Armed. Remember to run /sleepok when you're done."

Tạo file ~/.claude/commands/sleepok.md:

---
description: Disarm lid-safe mode
---

1. Run: `sudo /usr/bin/pmset -a disablesleep 0`
2. Verify: `pmset -g | grep SleepDisabled` — should read `SleepDisabled 0`
3. Report back: "Disarmed. The Mac will sleep on lid close again."

Từ giờ chỉ cần gõ /lidsafe hoặc /sleepok ngay trong Claude Code — kể cả từ điện thoại qua Remote Control.

Bước 2 — Status line để biết trạng thái hiện tại

Mình không muốn phải mở Terminal mỗi lần để kiểm tra trạng thái, nên viết một script rất nhỏ đọc pmset -g. Tạo file ~/.claude/statusline-keepawake.sh:

#!/bin/sh
if pmset -g | grep -qE "SleepDisabled[[:space:]]+1"; then
  echo "🔒☕ LID-SAFE"
else
  echo "😴 sleeps on close"
fi

Cấp quyền thực thi:

chmod +x ~/.claude/statusline-keepawake.sh

Khai báo statusline này trong ~/.claude/settings.json:

"statusLine": {
  "type": "command",
  "command": "~/.claude/statusline-keepawake.sh"
}

Bước 3 — Sudoers, để không phải gõ mật khẩu mỗi lần

Remote Control chạy từ điện thoại sẽ không có TTY để nhập mật khẩu. Nếu mỗi lần bật hay tắt đều yêu cầu nhập password thì toàn bộ workflow coi như vô nghĩa. Mở sudoers bằng lệnh sau:

sudo visudo -f /etc/sudoers.d/lidsafe

Thêm đúng một dòng này (thay tuan bằng username của bạn):

tuan ALL=(root) NOPASSWD: /usr/bin/pmset -a disablesleep 1, /usr/bin/pmset -a disablesleep 0

Điểm mình thích nhất ở cách này là phạm vi quyền rất nhỏ. Không có wildcard, không có NOPASSWD: ALL, cũng không mở toàn bộ pmset. Hệ thống chỉ được phép thực hiện đúng hai lệnh với đúng hai tham số mà mình cần.

Xong ba bước trên là có thể gõ /lidsafe ngay từ điện thoại, đóng nắp máy lại, và Claude Code vẫn chạy bình thường trên đúng môi trường của mình.

Một chút lan man, nhưng khá thú vị

Thực ra, thiết kế ban đầu của mình còn có thêm một bước nữa.

Ý tưởng khá đơn giản. Khi chạy /lidsafe, hệ thống sẽ bật disablesleep ở chế độ "tự động tắt". Đến lúc mình mở nắp máy trở lại, một wake hook sẽ phát hiện sự kiện đó và âm thầm khôi phục disablesleep về 0. Nếu muốn giữ nguyên trạng thái, mình sẽ có thêm một biến thể như /lidsafe-forced để bỏ qua bước tự động này.

Thế là mình bắt đầu dựng thử với sleepwatcher, một daemon nhỏ có thể cài bằng Homebrew để chạy script mỗi khi hệ thống sleep hoặc wake. Mình viết ~/.wakeup, tạo LaunchAgent, mọi thứ đều trông rất hứa hẹn.

Nhưng...

sleepwatcher không chủ động đi kiểm tra trạng thái máy. Nó chỉ đăng ký lắng nghe các sự kiện quản lý nguồn do hệ điều hành phát ra, khi disablesleep được bật, hệ điều hành không hề coi việc đóng nắp là một sự kiện sleep nữa. Không có sleep. Thì cũng không có wake.

Và nếu không có wake thì tất nhiên sẽ chẳng có hook nào được kích hoạt.

Nói cách khác, mình vừa dành cả buổi tối để xây một đầu báo cháy... cho một căn nhà mà mình đã bảo hệ thống đừng bao giờ báo có cháy. Muốn phát hiện việc đóng mở nắp ngay lúc đó chỉ còn hai lựa chọn. Một là liên tục polling AppleClamshellState. Hai là tự viết một chương trình dùng IOKit để theo dõi thay đổi của property này.

Cả hai đều quay trở lại đúng điều mình muốn tránh ngay từ đầu: thêm một tiến trình nền chạy liên tục chỉ để phục vụ một tính năng rất nhỏ.

Thế là mình bỏ luôn ý tưởng auto-disarm.

Trong lúc đào sâu vào mớ này, mình phát hiện một chi tiết khá vui: các đời MacBook hiện đại thực ra biết chính xác góc mở của màn hình, thậm chí đến mức rất nhỏ. Một thời gian trước từng có người reverse engineer API này.

https://github.com/samhenrigold/LidAngleSensor

Did you know that you have rights a lid angle sensor in your MacBook?

=)))))

Nhưng cuối cùng mình không dùng đến.

Sau khi tự mình vấp phải câu chuyện sleepwatcher, mình càng tin rằng với những thứ can thiệp trực tiếp vào hành vi ngủ của hệ thống thì càng đơn giản càng tốt. Một tín hiệu ổn định luôn đáng tin hơn một giải pháp thông minh nhưng nhiều biến số.

Điều cần lưu ý

disablesleep là một thiết lập hệ thống thật sự, không phải mẹo hay hack tạm thời. Khi bật nó lên, bạn cũng nên chấp nhận những đánh đổi đi kèm.

Điều đầu tiên là nhiệt độ.

Đóng nắp không khiến MacBook hỏng ngay lập tức, nhưng nó làm khả năng tản nhiệt kém hơn. Nếu chỉ chạy agent thêm hai mươi hoặc ba mươi phút để hoàn thành một tác vụ thì thường không có vấn đề gì. Ngược lại, nếu bạn định build project lớn, chạy Docker liên tục hay để AI agent làm việc hàng giờ, hãy để máy ở nơi thật thông thoáng. Đừng nhét vào balo, đừng kẹp giữa đống giấy tờ và cũng đừng để trong một ngăn kéo kín không có không khí lưu thông.

Nếu dùng MacBook Proworkload khá nặng, mình cũng khuyên nên để quạt chạy chủ động hơn. macOS vốn quản lý quạt khá tốt, nhưng những công cụ như Macs Fan Control có thể giúp quạt tăng tốc sớm hơn mặc định để giữ nhiệt độ ổn định. Đây không phải yêu cầu bắt buộc, nhưng là một lớp an toàn đáng cân nhắc nếu bạn thường xuyên để máy chạy lâu với nắp đóng.

Một điểm nữa cũng đáng nhớ: sau khi bỏ ý tưởng auto-disarm, sẽ không còn daemon nào nhắc bạn tắt chế độ này. Thứ duy nhất nhắc rằng disablesleep vẫn đang bật chính là status line trong Claude Code.

Vì vậy, khi xong việc, hãy chạy /sleepok.

Kết

Điều mình thích nhất ở Lid-Safe không phải là việc MacBook có thể chạy khi đóng nắp. Điều mình thích là cuối cùng mình không còn phải đối xử với chiếc laptop như một món đồ luôn phải mở hé để giữ cho agent sống.

MacBook có thể nằm yên trên bàn, trên giá đỡ hoặc ở một góc thông thoáng trong phòng. Mình cầm điện thoại, gửi lệnh cho Claude Code và để mọi thứ tiếp tục chạy trên đúng môi trường phát triển của mình.

Chỉ vậy thôi.

Không phải để máy không bao giờ ngủ.

Mà là để nó ngủ vào lúc mình quyết định, chứ không phải khi cái bản lề nói rằng đã đến lúc.