Mac Developer Bridge

by alexanderradahl

Not rated
GitHub

About

Give ChatGPT a real terminal on your Mac: shell, files, real PTY sessions, background jobs, and read-only Codex history over MCP.

Details

Author
alexanderradahl
Categories
Developer Tools

Setup

Install Mac Developer Bridge in your MCP client (Claude Desktop, Cursor, Windsurf, and others).

Repository: https://github.com/alexanderradahl/mac-developer-bridge

Follow the installation instructions in the repository README, then restart your MCP client.

Give ChatGPT a real terminal on your Mac.

Mac Developer Bridge turns a ChatGPT conversation into the reasoning layer for your actual Mac. It can run shell commands, edit files, start interactive terminal sessions, manage long-running jobs, read stored Codex threads without starting another Codex model turn, and optionally operate your real logged-in Chrome tabsin the background without stealing focus.

Example:“Find the Codex session I was working on yesterday, inspect the live repo, fix CI, push the result, and tell me what changed.”

That is the kind of workflow this project is built for.

[!WARNING] Mac Developer Bridge deliberately gives an MCP client the effective permissions of your macOS user. It isnot sandboxedand has no command or path allowlist. ReadSECURITY.mdbefore enabling it.

ChatGPT has the reasoning. Your Mac has the source code, terminal, credentials, build tools, local services, and work in progress. Mac Developer Bridge connects the two over MCP without adding another model or agent loop in the middle.

flowchart LR A[ChatGPT] -->|MCP| B[Mac Developer Bridge] B --> C[Shell, Git and local CLIs] B --> D[Filesystem] B --> E[Real PTY sessions] B --> F[Background jobs] B --> G[Stored Codex history] B --> H[Audit log and kill switch]

The bridge itself makes no OpenAI model call. It exposes deterministic local tools; ChatGPT supplies the reasoning. The Codex-history tools use read-onlycodex app-servermethods and never callturn/start.

- Recover a stored Codex thread, inspect the repo it refers to, and continue the work from ChatGPT.
- Run tests, builds, Git, package managers, database CLIs, AppleScript, and other tools already installed on your Mac.
- Keep interactive shells and terminal programs alive through a real PTY instead of pretending stdin is a terminal.
- Start long-running local jobs, inspect their logs later, and stop the whole process group.
- Read and modify files anywhere your macOS user can access.
- Optionally operate approved pages in your real logged-in Chrome profile without bringing Chrome to the foreground.

This is intentionally different from a local coding agent. There is no second reasoning loop. ChatGPT remains the agent; the Mac is the execution environment.

For a personal ChatGPT account, the menu-bar app is the easiest path. You need macOS, Node.js 18+,cloudflared, a hostname/tunnel, and ChatGPT Developer mode.

git clone https://github.com/alexanderradahl/mac-developer-bridge.git cd mac-developer-bridge ./menubar/build.sh open /Applications/MacDevBridge.app

UseStart, thenCopy ChatGPT Setupfrom the menu-bar app. The detailed OAuth and Cloudflare setup is inConnecting to ChatGPTandDEPLOY.md.

Workspace users who have access to OpenAI Secure MCP Tunnel can useinstall.shinstead. SeeTransports.

Want to see what to ask it to do? Start with thecopy-paste workflows.

If this is useful, star the repo so other developers can find it. If you build something interesting with it, share the exact workflow inWhat are you making ChatGPT do on your Mac?.

This is an independent open-source project and is not an official OpenAI or Cloudflare product. OpenAI, ChatGPT, Codex, and Cloudflare are trademarks of their respective owners.

Mac Developer Bridge is released under theMIT License. Bug reports and focused pull requests are welcome; seeCONTRIBUTING.md. Security-sensitive reports should follow the guidance inSECURITY.mdrather than being posted publicly.

- Arbitrary shell commands through/bin/zsh -lc, under the logged-in macOS user
- Detached background jobs with persistent stdout/stderr logs, status inspection, and process-group termination
- Unrestricted file read, write, append, list, stat, copy, move, chmod, symlink, mkdir, and recursive delete
- Unified-diff application throughgit apply
- Stored Codex thread discovery and reading without resuming a thread or starting a Codex model turn
- Paginated Codex turn retrieval for histories too large for a single response
- Local JSONL auditing
- Outbound-only private connectivity through OpenAI Secure MCP Tunnel, or a plain-HTTP loopback front end that Cloudflare Tunnel publishes over HTTPS
- Per-user persistence through a macOS LaunchAgent
- Fail-closed unlock latch:bridge.mjsre-reads the unlock file before every tool call, so removing it refuses the next call and exits — unless the process inheritedMAC_DEV_BRIDGE_FULL_ACCESS_ACK, which bypasses the file entirely
- Local kill switch (scripts/disable.sh), which stops the front end, the bridge, the optional background-Chrome native host, detachedshell_startjob groups, interactive pty sessions, and federated child MCP servers, verifying the same targets it signalled

Git, package managers, Vercel CLI, database CLIs, AppleScript, browser CLIs, build tools, and other installed programs remain reachable throughshell_exec; the bridge deliberately maintains no command allowlist.

Background Chrome without stealing focus

On macOS, the optional Background Browser integration operates thesame signed-in Chrome profile you already use, so existing website sessions work, but routine automation happens through a small local extension instead of AppleScript UI automation or Chrome DevTools Protocol page selection. The native host is bound at install time to the selected Chrome profile/account and refuses a signed-out or mismatched profile.

This is intentionally opt-in because authenticated browser control is powerful. Install the native host once, then load the unpacked extension once in Chrome:

Then in Chrome openchrome://extensions, enableDeveloper mode, chooseLoad unpacked, and select this repository'schrome-extension/directory. The expected extension id ispcebfblnmcappinbenkmddjdapaoajgm.

The extension keeps a Chrome-native tab group namedMDB. By default it targets eight extension-owned idle tabs. They are created only while Chrome is already foreground, then leased and reused for routine work. The group is collapsed when idle and expands while one or more tabs are leased. This mirrors the managed-group approach used by browser-agent extensions while avoiding a macOS/Chrome quirk measured on this project: evenchrome.tabs.create({ active:false })can bring Chrome to the foreground.

The pool now self-heals and self-expands. If Chrome or the extension restarts, or an older four-tab pool is still present, the extension grows the managed pool to the default eight tabs the next time younaturally focus Chrome. It never activates Chrome just to repair or expand itself. You can also force setup while Chrome is already foreground by callingchrome_workspace_setup(default pool size: 8).

chrome_workspace_statusis grantless because it only reads extension-owned local workspace state. It now includes lease age/idle metadata, the 10-minute idle-reclaim timeout, and the 20-second lease-wait budget.chrome_workspace_setupis also grantless because it creates only extension-owned idle pages; it refuses to create or expand the pool unless Chrome is already focused rather than stealing focus itself. Legacy/internaltabs.opencallers are routed to the sameworkspace.openlease path, so they cannot create loose tabs outsideMDB. When all tabs are busy,chrome_openwaits briefly for a release instead of failing immediately; abandoned leases are reclaimed after 10 minutes without browser activity, while every navigate/snapshot/click/fill renews an active lease.

Relaxed access is the default.Normal HTTP/HTTPS work through the signed-inMDBChrome profile does not require a terminal approval command or per-site allowlist. This is intentional: Mac Developer Bridge already exposes unrestricted shell/file authority as the logged-in macOS user, and the useful default is for browser execution to match that operator-chosen trust level while remaining background-first.

Relaxed approval doesnotrelax Chrome routing. Direct Chrome control throughshell_exec/shell_start— AppleScript, JXA, direct Chrome executable launches, or shellopenof an HTTP/HTTPS URL (includingopen -g) — is always refused withCHROME_BACKGROUND_REQUIRED, in both Relaxed and Strict modes. Browser work must use thechrome_tools and the managedMDBgroup. This keeps the no-focus-stealing behavior structural instead of depending on which approval mode is selected.

If you want a tighter browser/app workflow, enableStrict approvalsfrom the Mac Developer Bridge menu-bar app. The toggle is live; no restart is needed. In Strict mode,chrome-backgroundapprovals are additive and shared across every ChatGPT session connected to the bridge until each grant expires:

./scripts/approve-personal-browser.sh \ --provider chrome-background \ --url-pattern 'https://www.producthunt.com/' \ --url-pattern 'https://www.reddit.com/*' \ --ttl 900

- chrome_openan approved URL into an idle tab leased from theMDBgroup.
- chrome_snapshotto read the page and get stable-enough selectors for visible controls.
- chrome_fill/chrome_click/chrome_navigateas needed.
- chrome_closeto return the workspace tab to its idle extension page and release the lease. Workspace release is local/grantless cleanup, so Strict-mode URL grants cannot strand a finished lease.

Profile binding is always enforced. In relaxed mode the extension permits normal HTTP/HTTPS sites without a per-site grant. In Strict mode, eachchrome-backgroundapproval is stored as its own mode-0600 file under$DATA_DIR/chrome-background-grants/, expires after at most 15 minutes, and is merged with other still-live approvals. Expired files are pruned automatically and URL patterns are enforced inside Chrome. Federated personal-browser providers keep their separate single-use behavior.

chatgpt_extension_statusis deliberately read-only. It reports the installed ChatGPT Chrome extension version, the localcom.openai.codexextensionnative-host registration, and—when achatgpt.comtab is already open—the live status returned by OpenAI's own page bridge. MDB doesnotpatch the OpenAI extension, add itself to the OpenAI native-host allowlist, expose arbitrary private OpenAI RPC calls, or programmatically open the ChatGPT side panel. The current ChatGPT extension does not declareexternally_connectable; its side-panel open path also requires a trusted user gesture.

What background mode doesnotpromise: CAPTCHAs, native browser/OS permission dialogs, file pickers, downloads requiring a trusted user gesture, passkeys, and other browser security UI may require a foreground/manual step. The bridge reports that limitation rather than silently activating Chrome. This is also deliberately narrower than arbitrary page JavaScript or network-header capture; seeSECURITY.md.

./scripts/uninstall-background-chrome.sh

For native macOS apps, MDB stillprefersbackground-capable APIs or web paths because Accessibility/AppleScript automation of apps such as Slack may require the target application to become frontmost. In the default relaxed mode, non-Chrome native app control is allowed without a separate terminal approval, so MDB can still complete the task when a foreground app interaction is genuinely necessary. Chrome is the exception: because MDB has a dedicated signed-in background extension, direct Chrome GUI automation is always forced back to theMDBbrowser path rather than allowed to steal focus.
- an API or MCP connector for the service;
- the service's web app through the signed-inMDBChrome group;
- native-app GUI automation only when foreground interaction is genuinely required.

WhenStrict approvalsis enabled, native foreground app control is blocked unless the operator creates a one-use, app-scoped grant:

./scripts/approve-foreground-gui.sh --app Slack --ttl 60

Strict mode is optional and off by default. The menu-bar checkbox changes it live.

A real pty, allocated bylib/ptyhelper.pl(core Perl, no dependency added). Advertised only when the helper runs on this host; otherwise the six tools are absent rather than broken.

Limits that will be visible in normal use:

- Line length.While the terminal is in canonical mode — the default, and what every interactive prompt uses — the line disciplinediscardsan input line of 1024 bytes or more instead of truncating it.pty_writerefuses such a write withPTY_WRITE_CANON_LIMITrather than reporting bytes the program will never see. Bytes accumulate across calls until a\ror\n, so chunking does not evade it. Send lines of at most 1023 bytes. A session that has put its terminal in raw mode is checked and allowed.
- Concurrency.The session cap is taken, not merely checked, so concurrentpty_startcalls cannot exceed it.
- Retention.Each session keeps the lastMAC_DEV_BRIDGE_PTY_RING_BYTESof output in a fixed ring;pty_readreportslostByteswhen a cursor falls behind it.
- Containment.See SECURITY.md —pty_closereportsleaderGroupGone,ttyProcessesKilledanduncontainedPidsseparately, andcontainmentVerifiedis true only when nothing survived.

If a provider registry is configured, each provider's tools are advertised with akey__toolprefix and proxied. There is no built-in provider: the registry is operator-supplied. Personal-browser-profile mode requires a per-use operator grant — see SECURITY.md.

These are read bybridge.mjson both transports.

The MCP server runs with the effective permissions of the macOS account that launches it. It has no path allowlist, shell-command allowlist, sandbox, or internal per-command approval gate.

macOS still enforces TCC privacy controls, Full Disk Access, ACLs, SIP, Keychain access controls, andsudoauthentication. Non-interactive MCP shell calls do not magically provide a sudo password or a terminal UI. Configure passwordlesssudoonly when you deliberately want that separate escalation.

The bridge refuses to start until a deliberate acknowledgement exists, and re-checks it before every tool call — so removing the acknowledgement file both prevents future starts and stops a running bridge at its next call.

The environment form (MAC_DEV_BRIDGE_FULL_ACCESS_ACK) is deliberatelynotrevocable that way: a bridge that inherited it never reads the file, so deleting the file does not stop it. The Install steps below export that variable, so a bridge started from such a shell is only stoppable by stopping the process. The menu bar app strips it from its children for exactly this reason.

ChatGPT action permissions and confirmation behavior are separate. The MCP server advertises write and destructive annotations honestly and cannot bypass restrictions enforced by the ChatGPT product or workspace.

The bridge speaks MCP over stdio. Two transports can carry it to ChatGPT.

OpenAI Secure MCP Tunnel(install.sh, documented below) is outbound-only and needs no public endpoint. It requires the Tunnel connection type in ChatGPT's plugin dialog, which isnot available on personal accounts— the option renders but is disabled.

Cloudflare Tunnel + Server URL(mcp-http.mjs) is the fallback when Tunnel is unavailable.mcp-http.mjsfronts the bridge with Streamable HTTP on127.0.0.1:8787behind OAuth 2.1 (and a static bearer for other clients), andcloudflaredpublishes it:

export MAC_DEV_BRIDGE_HTTP_TOKEN="$(openssl rand -hex 32)" node mcp-http.mjs

ChatGPT's plugin dialog offers Authentication:OAuth,No Auth, orMixed— there is no API-key/bearer field.mcp-http.mjstherefore implements an OAuth 2.1 authorization server as well, and that is how you connect ChatGPT. SeeConnecting to ChatGPTbelow. The static bearer token still works for any client that can send anAuthorization: Bearerheader.

The host is pinned to loopback and the path to/mcp, deliberately — the only intended peer iscloudflaredon the same machine.

Understand the difference in exposure before choosing this one. The Tunnel transport makes only outbound connections. This one publishes an HTTPS endpoint that fronts unrestricted shell access, with a single bearer token as the entire barrier. Rotate the token if it is ever disclosed, and consider Cloudflare Access in front of it for a second factor.

Not yet automated for this transport:install.shrequirestunnel-clientand rejects a missingtunnel_...id, so it cannot install the HTTP path, and there is no LaunchAgent — nothing restartsmcp-http.mjsorcloudflaredafter a reboot or a crash.scripts/doctor.shdoes cover this transport.uninstall.shremoves the files but does not stop a running front end.

ChatGPT's plugin dialog offers three Authentication choices —OAuth,No Auth,Mixed— and no API-key/bearer field, so the static bearer token has nowhere to be entered.mcp-http.mjstherefore implements an OAuth 2.1 authorization server, and that is how ChatGPT connects.

The menu bar app'sCopy ChatGPT Setupproduces this list pre-filled.

Untick OIDC because/.well-known/openid-configurationis served only as an alias of the OAuth metadata and deliberately omits every signing and subject field. No ID token is issued, so an OIDC-strict client should abort rather than demand one.

ChatGPT then opens a consent page served by your own machine. It names the exact callback it will redirect to and asks for the bridge token, which is how it knows the approval came from you.Read the "Will redirect to" line before approving— any/connector/oauth/<token>path is a valid ChatGPT connector, including one someone else created.

Use anamedCloudflare tunnel. A quick tunnel's hostname changes on every start, and that hostname is the OAuth issuer — so a restart between discovery and callback makes the issuer stop matching what ChatGPT recorded, and a strict client drops the callback silently. A named tunnel also means creating the connector once instead of every run.

Endpoints served:/.well-known/oauth-protected-resource,/.well-known/oauth-authorization-server,/.well-known/openid-configurationplus/.well-known/oauth-protected-resource/mcp,/.well-known/oauth-authorization-server/mcp,/.well-known/openid-configuration/mcpand/mcp/.well-known/openid-configuration— seven paths in total, since the/mcp/-prefixed form exists only foropenid-configuration. Then/authorize,/token,/revoke,/revoke-all, and/healthz. A 401 from/mcpcarriesWWW-Authenticate: Bearer resource_metadata="…", which is what lets a client discover the rest.

menubar/builds a small AppKit status-bar app that owns the two processes this transport needs and surfaces the three things you actually use: the public URL, the bearer token, and whether the endpoint is answering.

./menubar/build.sh # also installs a copy to /Applications open /Applications/MacDevBridge.app

The build installs to/Applications(falling back to~/Applications) because Launchpad and Spotlight do not surface apps living in~/Downloads. The bundle locatesmcp-http.mjsviaMAC_DEV_BRIDGE_HOME, then a package next to itself, then a path baked intoInfo.plistat build time — so the installed copy still finds the package.

The menu gives you: current status, the tunnel mode,Copy Server URL,Copy OAuth Client ID,Copy ChatGPT Setup(the whole dialog filled in, in order),Copy Bearer Token, Start/Stop, a liveStrict approvalscheckbox (off by default), Rotate Token, Open Logs, and Quit.

It prefers anamedCloudflare tunnel when~/.cloudflared/config.ymldeclares one, giving a stable URL — otherwise a quick tunnel, whose hostname changes every start and forces the ChatGPT connector to be recreated each time. The menu shows which mode is active.

Why it is worth using over the raw commands:

- It is the supervisor. Start spawnsmcp-http.mjsandcloudflared; Stop and Quit stop exactly what it started, rather than discovering processes by name.
- Start writes the unlock file and Stop removes it, so stopping is fail-closed throughbridge.mjs's per-call latch, not merely a process kill.
- The token lives in a mode-0600 file and is passed byMAC_DEV_BRIDGE_HTTP_TOKEN_FILE, keeping it out ofps eww.
- Status is polled from/healthzand from the child processes' liveness, so a child dying is reported rather than assumed away.
- On launch it reclaims orphans —bothchildren.applicationWillTerminatedoes not run on a force-quit, crash, or hard reboot, so a previous run could leave the unlock file armed, the front end serving, andcloudflaredstill publishing a public hostname. Launching disarms the latch and stops whatever is recorded inmcp-http.pidandcloudflared.pid, each identity-checked first because pids get recycled and those files surviveSIGKILLand reboot. Reclaiming only the front end previously left a public ingress that no later run could close, and that the next Start would re-arm alongside a second tunnel.
- It never passesMAC_DEV_BRIDGE_FULL_ACCESS_ACKto its children. That variable is a standing unlock inbridge.mjs, so inheriting it would make Stop unable to revoke anything — and the install docs tell you to export it.
- One child dying stops the other. Reporting a failure while leaving the sibling alive leftcloudflaredpublishing with the latch still armed and the menu reading "not running".
- Children inherit thelogin shellPATH, soshell_execbehaves the same as it does in a terminal (a GUI-launched app otherwise has no nvm or Homebrew).

The app is ad-hoc signed and not notarized. It locatesmcp-http.mjsviaMAC_DEV_BRIDGE_HOME, then a package next to the bundle, then a path baked intoInfo.plistat build time — so the/Applicationscopy works with the package left where it is. Rebuild after moving the package so the baked path stays correct.MAC_DEV_BRIDGE_HOME.

It does not replacescripts/disable.sh: detachedshell_startjobs outlive the front end by design, and only that script reclaims them from the job registry.
- macOS and a logged-in desktop user.
- Node.js 18 or newer.
- ChatGPT Developer mode.
- A workingcodexCLI only for the three Codex-history tools. Shell and filesystem access do not depend on Codex.

OpenAI Secure MCP Tunnel additionally requires:
- The officialtunnel-clientbinary, downloaded from OpenAI Platform Tunnels or the official OpenAI GitHub release, executable and available onPATHor at~/.local/bin/tunnel-client.
- An OpenAI tunnel ID scoped to the ChatGPT workspace that will use it.
- A runtime API key whose principal has Tunnels Read + Use.
- TheTunnelconnection option in the ChatGPT plugin dialog.

Cloudflare Tunnel + Server URL additionally requires:
- cloudflared, authenticated to a Cloudflare account.
- A hostname you control, or a quick-tunnel URL.
- opensslfor generating the bearer token.
- TheServer URLconnection option withOAuth— seeConnecting to ChatGPT. There is no No-Auth mode:/mcpis hardcoded with no override, andmcp-http.mjsrefuses to start without a token.

No reviews yet — be the first

Sign in to leave a review

Use Google, GitHub, or an email account so ratings stay tied to real people.

Email sign in

No reviews posted yet.