Build1 publisher2 min readPublished
Cloning Chrome under a second bundle ID stops Playwright jobs from holding the human's window
Four Playwright jobs on one Mac held com.google.Chrome, so a Dock launch came back with error -600 while every job log stayed clean. The repair also had to survive npm install, which keeps resetting the browser path inside node_modules.
The Engineer · Build desk

What happened
- A Dock launch of Google Chrome on the author's Mac returned error code -600 at 13:31 JST on September 1, 2026, and no window came up.
- Three headless Chrome processes and one headful one, four in all, were running at that moment, every one of them spawned by the same Playwright automation.
- macOS LaunchServices treats apps sharing a CFBundleIdentifier as one instance of one app, and Google Chrome's identifier is com.google.Chrome.
- The jobs themselves reported nothing wrong, exiting 0 with clean logs, so the breakage surfaced only when a person tried to use the browser.
- The repair is a launchd job, com.shun.chrome-automation-repair, set to StartInterval 21600 with RunAtLoad true, which re-applies the automation browser configuration.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- exposure Job-level monitoring cannot see this failure class, because the runner reports success; the first signal comes from a person finding a dead Dock icon, so there is no threshold to alert on.
- constraint Kill-on-exit puts a shutdown-timing obligation on every job and reopens the same gap each time a job is added; the bundle split is enforced once, by LaunchServices.
- cost The isolation is paid for with a second full copy of Chrome, which only picks up an update when the script sees the two version strings diverge and re-copies the app.
- decision Anyone running Playwright's channel: 'chrome' on a machine a human also uses now has to decide whether to keep patching a file inside node_modules on a schedule.
Two of the three conditions here are properties of the setup, not of the OS. Every job exec'd /Applications/Google Chrome.app/Contents/MacOS/Google Chrome directly, so the automation and the person at the keyboard were opening one app bundle [4]. A headless job also has no window for LaunchServices to raise, so the Dock request resolved to an instance with nothing to show and the shell returned -600 [6]. The post identifies that code as procNotFound [7].
A retry loop is no use against this, because the runner sees a successful run [8]. The post also gives the scaling behaviour: as the job count rises, Chrome being held by one job or another stops being an event and becomes the steady state [9].
The first fix considered was making each job kill Chrome when it finished. That distributes shutdown timing across every job, and by the post's account each job added later opens a fresh gap [10]. The second was to change the name: copy Chrome to /Applications/Chrome Automation.app and set its bundle ID to com.google.ChromeAutomation, which LaunchServices then tracks separately from com.google.Chrome, so a human's `open -a "Google Chrome"` resolves to an identifier no automation process is holding [11]. "Resource contention disappears the moment you separate the names," the author wrote [12].
Then Playwright gets in the way. The `channel: 'chrome'` option writes the launched browser's path into playwright-core/lib/coreBundle.js [13], and that file sits inside node_modules, so any npm install overwrites it and points the path back at /Applications/Google Chrome.app [14]. A StartInterval of 21600 seconds is six hours, which allows four runs in a day, and a revert that lands just after a run stays in place for up to six hours [18][19].
The repair script checks two things before it copies anything. keychain_gate() warns when --use-mock-keychain is not set; warning is all it does [16]. It then compares the version of Google Chrome.app against the version of Chrome Automation.app, skips when they match, and rebuilds the clone with cp -R when they differ or the clone is absent [17]. The published listing is truncated mid-command, so the rest of the rebuild is not shown [20].
For any of this to transfer you need the same shape: automation that launches the human's Chrome bundle, on a Mac a human also uses. The post's revenue figure, zero to 1.2 million yen a month in six months, is an income number [3]; the load figure is four Chrome processes at once on one workstation [2].
What to watch
- A supported Playwright option for pointing channel: 'chrome' at a custom bundle path would remove the need to patch a file inside node_modules on a timer.
- How often the automation clone runs a Chrome version behind the human's copy, given the version check fires every six hours.
- Whether the rebuild step re-applies the com.google.ChromeAutomation bundle ID after each cp -R, which the truncated listing does not show.