#!/bin/sh : <<'CODECONNECT_HTML_END' CodeConnect - Every coding agent, at a glance

CodeConnect

Every coding agent, at a glance.

Everything they are doing, everything they need, on one screen in your pocket.

Download CodeConnect Remote on the App StoreFree on the App Store · iPhone · needs a Mac
Every session on one screenUnblock with a tapReview diffs on the phoneTake over the terminal when you need to

See everything at once.

Every agent on your Mac, what it's working on, and which one is waiting on you. No tab-hunting, no guessing.

Unblock it in seconds.

When an agent needs a yes, your phone buzzes. You see the exact command and answer it. The agent carries on, you carry on.

Review before you're back.

Read the diff, leave a note on any hunk, and the agent picks it up. By the time you're at the desk, the fix is in.

Built for every coding agent.

CodeConnect sits beside the agent, not inside it. Each one behaves exactly as it does today, and simply shows up on your phone.

Available now

Claude Code

Fleet, approvals, questions, diffs, terminal. Everything.

Coming soon

Codex

In development on the same daemon. Same phone, same screen.

On the roadmap

More agents

Other terminal agents follow, starting with observation.

Nothing changes on the Mac.

Start Claude Code the way you always do, with one extra word. It behaves exactly as before, and the session simply shows up on your phone too. There is no server in between. The phone talks straight to your Mac over your own private network.

$ codeconnect claude
✓ session on your phone as app-1

Claude Code - ready
› ship the release branch and open a PR_
Built to run on your tailnet

Your Mac, reachable only by you.

CodeConnect uses Tailscale by default. Your phone reaches your Mac over your own tailnet, from cellular, a coffee shop, another city, and every hop is WireGuard-encrypted before it touches a network. There is no CodeConnect server in between, and no account to create.

Encrypted end to end

Traffic between phone and Mac rides WireGuard, with TLS on top when your tailnet has HTTPS certificates enabled.

Private by default

The daemon listens on your Tailscale address and loopback, nothing else. It is never exposed to the open internet.

Works from anywhere

If your phone is on your tailnet, it can reach your Mac. No port forwarding, no VPN config, nothing to set up.

Tailscale is a registered trademark of Tailscale Inc. CodeConnect is not affiliated with Tailscale. Any private network you operate also works.

STEP 1 · ON YOUR PHONE

Get the app.

Download CodeConnect Remote on the App Store

Free. iOS 17 or later.

STEP 2 · ON YOUR MAC

Install the daemon.

codeconnect.sh - sh
$ curl -fsSL https://codeconnect.sh | sh

macOS 13 or later · Apple silicon or Intel · needs tmux

Then codeconnect daemon install, codeconnect pair, scan the code from the app, and start sessions with codeconnect claude instead of claude. Claude Code behaves exactly as before.

CODECONNECT_HTML_END # CodeConnect installer — https://codeconnect.sh # # Usage: # curl -fsSL https://codeconnect.sh | sh # # **Why this file starts with HTML.** It is one file with two readers: a # browser renders the page above; `sh` skips it, because the whole block sits # inside the quoted heredoc opened on line 2 — `:` is a no-op fed the HTML as # input it never reads. One URL, one set of bytes, no User-Agent sniffing — # there is no cache or spoofing path on which a browser gets shell or a shell # gets served something it wasn't shown. The sentinel line above is the only # other top-level statement this file is allowed; everything else stays # behind the function gate described below. A download truncated anywhere in # the HTML leaves the heredoc unterminated, and sh executes nothing at all. # # **Why every action lives inside a function.** # # `curl | sh` hands bytes to the shell as they arrive, so the shell can begin # executing line 1 while line 200 is still in flight. A dropped connection # halfway through an ordinary streaming script leaves the first half already # executed — and the first half is where the deleting happens. `set -e` cannot # help: the mutation already ran. # # So: this file defines functions and does nothing else, and the only # invocation is the literal last line. Truncate anywhere inside a function and # `sh` fails to parse the file at all, before running a single command. # Truncate after a function's closing brace and you get a defined function # nobody calls. Neither outcome touches the disk. # # The consequence for maintainers: **no top-level command may be added above # that final line.** Not an `echo`, not a variable assignment that shells out, # not a "quick" trap. The test suite enforces this. # # The other consequence: success must be *stated*, never inferred from silence. # A truncated run exits 0 having done nothing, so "no error" is not evidence of # an install. Look for the final summary block. CODECONNECT_INSTALLER_VERSION="1.4.5" CODECONNECT_REPO="faisalmumtaz89/CodeConnect" CODECONNECT_REPO_URL="https://github.com/faisalmumtaz89/CodeConnect.git" CODECONNECT_API="https://api.github.com/repos/faisalmumtaz89/CodeConnect/releases/latest" # The Apple Team ID whose signature is accepted on release binaries. # # Empty means the release path is not configured, and the installer refuses it # rather than falling back to an unverified install. Fail closed: an installer # that quietly stops checking signatures is worse than one that does not run. CODECONNECT_TEAM_ID="2XL2264MC8" # **Copied verbatim from `signature_requirement()` in # `mac/codeconnect/src/update_install.rs`, and it must stay that way.** The # installer and `codeconnect update` are the two doors into the same # directory; a machine that accepts a binary at install time which the updater # would later reject — or the reverse — is worse than either rule alone. # # Three clauses, and dropping any one makes it meaningless: # # * `anchor apple generic` — the chain ends at Apple. Without it the whole # expression is satisfiable by a certificate anybody can mint. # * the two OIDs — the issuing CA is Apple's *Developer ID* CA and the leaf # is a *Developer ID Application* certificate. Without them any Apple # certificate qualifies, including the free Apple Development certificate # every developer account can issue in a minute. # * `subject.OU` — that certificate belongs to *this* team. CODECONNECT_REQUIREMENT="anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] exists and certificate leaf[field.1.2.840.113635.100.6.1.13] exists and certificate leaf[subject.OU] = \"$CODECONNECT_TEAM_ID\"" # Fixed, and not a matter of taste — see the note in `install_release`. CODECONNECT_PREFIX="${CODECONNECT_HOME:-$HOME/.codeconnect}" # ------------------------------------------------------------------ output # # Everything goes to stderr. stdout is reserved so a caller can pipe this # script's *result* somewhere without status chatter contaminating it. cc_say() { printf '%s\n' "$*" >&2; } cc_step() { printf '\n\033[1m%s\033[0m\n' "$*" >&2; } cc_warn() { printf 'warning: %s\n' "$*" >&2; } # Failure is loud, names what did and did not happen, and gives exactly one # next action. It never speculates about a cause it did not observe. cc_die() { printf '\nerror: %s\n' "$1" >&2 shift for line in "$@"; do printf '%s\n' "$line" >&2; done exit 1 } cc_usage() { cat >&2 <<'USAGE' CodeConnect installer curl -fsSL https://codeconnect.sh | sh Options: --reinstall Replace an existing install. Normally an existing install is left to `codeconnect update`, which is the product's own updater and does the job better. --source Build from a source checkout instead of installing a release. Requires a Rust toolchain and takes several minutes. --version Print the installer version and exit. --help Print this message and exit. Installs to ~/.codeconnect/bin. Never uses sudo. Never edits your shell configuration. Never enables a system service — it prints the command and lets you decide. Audit before running: curl -fsSL https://codeconnect.sh -o codeconnect-install.sh less codeconnect-install.sh sh codeconnect-install.sh USAGE } # ------------------------------------------------------------------- tools # Children never inherit the pipe. stdin *is* this script when invoked as # `curl | sh`, so any child that reads stdin would eat the installer's own # remaining bytes — and `git` and `cargo` both read stdin under some # conditions. cc_run() { "$@" /dev/null 2>&1; } # `-q` first and always: without it curl reads ~/.curlrc, which can add URLs, # attach headers or redirect output. # # `--proto '=https'` governs the *initial* request and `--proto-redir '=https'` # governs every hop after it — without the second, a redirect can walk the # download down to plaintext, which is exactly the hop worth attacking. # `--max-filesize` bounds a response that lies about its size. cc_fetch() { cc_run curl -q --proto '=https' --proto-redir '=https' --tlsv1.2 \ -fsSL --max-time 300 --max-filesize "$3" "$1" -o "$2" } cc_fetch_stdout() { cc_run curl -q --proto '=https' --tlsv1.2 -fsSL --max-time 30 --max-filesize 262144 "$1" } # One string field out of a JSON object, without requiring jq. Deliberately # narrow: it matches `"key": "value"` and nothing cleverer, because the only # document it reads is a GitHub release object and a parser that guesses is a # parser that lies. cc_json_string() { sed -n 's/.*"'"$2"'"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$1" | head -1 } # --------------------------------------------------------------- preflight # Collected in one pass rather than one-at-a-time: a user missing three tools # should learn that once, not across three failed runs. # Built with a command substitution rather than an embedded literal newline: # a definition that spans lines is one a truncated download can split in half. cc_missing="" cc_need() { cc_missing="$(printf '%s\n - %s' "$cc_missing" "$1")"; } # Written as a full `if` rather than `[ ... ] && return 0`. Under `set -e` an # AND-OR list whose result is non-zero aborts the shell, so the short form # would exit silently on the branch that most needs to print something. cc_preflight_common() { if [ "$(id -u)" = "0" ]; then cc_die "do not run this installer as root." \ "" \ "CodeConnect installs into your own home directory and needs no" \ "elevated privileges. Rerun it without sudo." fi } cc_preflight_platform() { os="$(uname -s)" if [ "$os" != "Darwin" ]; then cc_die "CodeConnect is macOS-only; this machine reports '$os'." \ "" \ "Nothing was installed." fi arch="$(uname -m)" case "$arch" in arm64 | x86_64) ;; *) cc_die "unrecognised architecture '$arch'." \ "" \ "CodeConnect ships for arm64 and x86_64. Nothing was installed." ;; esac if [ -z "${HOME:-}" ] || [ ! -d "$HOME" ]; then cc_die "\$HOME is not set to a directory that exists." \ "" \ "Nothing was installed." fi } # tmux is a *runtime* dependency, not a build one: the daemon installs fine # without it and then cannot host a single session. So this warns rather than # refuses — refusing would overreach on an install that is genuinely valid, # and staying silent would let the user discover it at the worst moment. cc_check_tmux() { if cc_have tmux; then return 0 fi cc_warn "tmux is not installed." cc_say " CodeConnect hosts every session in a private tmux server, so" cc_say " \`codeconnect claude\` will not start one until tmux exists." if cc_have brew; then cc_say " Install it with: brew install tmux" else cc_say " Install it via Homebrew (https://brew.sh) or your package manager." fi } # ----------------------------------------------------------------- locking # `mkdir` is the atomic primitive every POSIX filesystem agrees on. A stale # lock is never broken automatically: two installers racing on the same # prefix is exactly the situation where guessing is worst. cc_lock_dir="" cc_stage_dir="" # Full `if`s, not `[ … ] && …` chains. `set -e` is in force inside a trap # handler too, so a chain whose first test is false aborts the handler — which # would have skipped the lock removal below on every early exit, leaving the # next run to report an install that is not running. cc_cleanup() { if [ -n "$cc_stage_dir" ] && [ -d "$cc_stage_dir" ]; then rm -rf "$cc_stage_dir" fi if [ -n "$cc_lock_dir" ] && [ -d "$cc_lock_dir" ]; then rmdir "$cc_lock_dir" 2>/dev/null || true fi return 0 } cc_acquire_lock() { cc_lock_dir="$CODECONNECT_PREFIX/.install-lock" if ! mkdir "$cc_lock_dir" 2>/dev/null; then cc_lock_dir="" cc_die "another CodeConnect install is already running." \ "" \ "If you are certain none is, remove the lock and rerun:" \ " rmdir $CODECONNECT_PREFIX/.install-lock" \ "" \ "Nothing was changed." fi trap cc_cleanup EXIT HUP INT TERM } cc_prepare_prefix() { if [ -e "$CODECONNECT_PREFIX" ] && [ ! -d "$CODECONNECT_PREFIX" ]; then cc_die "$CODECONNECT_PREFIX exists and is not a directory." \ "" \ "Nothing was changed." fi if [ -L "$CODECONNECT_PREFIX" ]; then cc_die "$CODECONNECT_PREFIX is a symlink." \ "" \ "This installer will not write through a symlinked prefix." \ "Nothing was changed." fi mkdir -p "$CODECONNECT_PREFIX" } # ------------------------------------------------------- existing installs # An existing install is handed to `codeconnect update`, not replaced here. # # This installer is a bootstrap. `codeconnect update` is the product's own # updater: it compares versions across all three binaries, refuses a # downgrade, exchanges the whole directory in one step so a power cut cannot # leave a mixed set, and restarts a managed daemon. Reimplementing a worse # version of that on the upgrade path would be the wrong trade. # # `--reinstall` is the escape hatch for a set that is broken badly enough that # its own updater cannot run. cc_guard_existing() { if [ ! -x "$CODECONNECT_PREFIX/bin/codeconnect" ]; then return 0 fi if [ "$cc_reinstall" = "yes" ]; then return 0 fi existing="$(cc_run "$CODECONNECT_PREFIX/bin/codeconnect" --version 2>/dev/null || echo 'unreadable')" cc_die "CodeConnect is already installed on this Mac." \ "" \ " $existing" \ "" \ "To move it to the latest release, use its own updater — it swaps all" \ "three binaries in one step and restarts a managed daemon:" \ "" \ " $CODECONNECT_PREFIX/bin/codeconnect update" \ "" \ "If that install is broken badly enough that its updater will not run," \ "rerun this with --reinstall." \ "" \ "Nothing was changed." } # -------------------------------------------------------- release resolve cc_resolve_release() { cc_step "Resolving the latest CodeConnect release…" body="$cc_stage_dir/release.json" if ! cc_fetch_stdout "$CODECONNECT_API" >"$body" 2>/dev/null; then cc_die "could not reach the GitHub Releases API." \ "" \ "This means one of: no network, GitHub is unreachable, the API" \ "rate-limited this IP, or the repository is not public. The" \ "request does not distinguish them, so this message will not" \ "guess." \ "" \ "Nothing was installed." fi cc_tag="$(cc_json_string "$body" tag_name)" if [ -z "$cc_tag" ]; then cc_die "the Releases API returned no tag for a latest release." \ "" \ "CodeConnect may not have published one yet." \ "Nothing was installed." fi # `v0.4.0` -> `0.4.0`. The tag is the contract; the bare version is what # asset names are built from. cc_version="${cc_tag#v}" case "$cc_version" in *[!0-9.]* | "") cc_die "the latest release tag '$cc_tag' is not a plain version." \ "" \ "Nothing was installed." ;; esac cc_say " latest release: $cc_tag" } # --------------------------------------------------------- release install # Runs before anything is created, so that "nothing was changed" is literally # true rather than true-about-binaries. An empty directory left behind by a # refusal is a small lie, and this codebase does not get to tell small ones. cc_preflight_release() { if [ -z "$CODECONNECT_TEAM_ID" ]; then cc_die "this installer has no signing identity configured." \ "" \ "Release installs verify an Apple Developer ID signature, and" \ "the Team ID to check against is not set in this build of the" \ "installer. It will not install an unverified binary." \ "" \ "Build from source instead, if you have a Rust toolchain:" \ "" \ " curl -fsSL https://codeconnect.sh | sh -s -- --source" \ "" \ "Nothing was changed." fi } cc_install_release() { cc_resolve_release archive="codeconnect-${cc_version}-macos-universal.tar.gz" base="https://github.com/${CODECONNECT_REPO}/releases/download/${cc_tag}" cc_step "Downloading ${archive}…" cc_fetch "$base/$archive.sha256" "$cc_stage_dir/$archive.sha256" 4096 || cc_die "could not download the checksum for $cc_tag." "" "Nothing was installed." cc_fetch "$base/$archive" "$cc_stage_dir/$archive" 104857600 || cc_die "could not download $archive." "" "Nothing was installed." # Integrity, not authenticity. Checksum and archive share an origin, so # this catches a truncated or corrupted download and nothing more. The # signature check below is the one that survives a hostile origin. cc_step "Verifying the download…" (cd "$cc_stage_dir" && cc_run shasum -a 256 -c "$archive.sha256" >/dev/null 2>&1) || cc_die "$archive failed its checksum." \ "" \ "The download is corrupt or was tampered with in transit." \ "Nothing was installed." cc_say " checksum ok" cc_run tar -xzf "$cc_stage_dir/$archive" -C "$cc_stage_dir" || cc_die "could not unpack $archive." "" "Nothing was installed." unpacked="$cc_stage_dir/codeconnect-${cc_version}" [ -d "$unpacked" ] || cc_die "$archive did not contain codeconnect-${cc_version}/." "" "Nothing was installed." # **The check that matters.** A bare `codesign --verify` only proves a # signature is internally intact, which an attacker's own signature also # satisfies. The pinned requirement is what makes this a statement about # *who* signed it. # # `--all-architectures` is not optional here. A universal binary carries # one signature per slice, and codesign otherwise checks only the slice # this Mac would run: the x86_64 half of a universal file could be # replaced wholesale and an arm64 Mac would never look at it. # # There is no `--require-timestamp` verify flag — passing one makes # codesign exit on a usage error, which a `|| cc_die` then reports as a # bad signature. The release workflow checks the timestamp separately, by # reading `codesign --display` output, and that is the right place for it. # # `-R` rather than `spctl`: spctl assesses against system policy, whose # behaviour on a non-quarantined bare Mach-O depends on state this script # does not control, and which needs the network. An explicit requirement # is deterministic and offline. cc_step "Verifying the signature…" for binary in codeconnect ccd cc-hook; do cc_run codesign --verify --strict --all-architectures \ -R="$CODECONNECT_REQUIREMENT" \ "$unpacked/$binary" >/dev/null 2>&1 || cc_die "$binary is not signed by CodeConnect (team $CODECONNECT_TEAM_ID)." \ "" \ "Refusing to install it. Nothing was installed." done cc_say " signed by $CODECONNECT_TEAM_ID, all slices" cc_place_binaries "$unpacked" cc_installed_from="release $cc_tag" } # ---------------------------------------------------------- source install cc_install_source() { cc_step "Checking build prerequisites…" cc_missing="" cc_have git || cc_need "git" cc_have cargo || cc_need "a Rust toolchain (https://rustup.rs)" cc_run xcode-select -p >/dev/null 2>&1 || cc_need "Xcode command line tools (xcode-select --install)" if [ -n "$cc_missing" ]; then cc_die "building from source needs tools this Mac does not have:" \ "$cc_missing" \ "" \ "Install them and rerun. Nothing was installed." fi cc_say " git, cargo and the command line tools are present" src="$CODECONNECT_PREFIX/src" if [ -e "$src" ]; then cc_die "$src already exists." \ "" \ "This installer will not build over a directory it did not" \ "create. Inspect it, move it aside, and rerun." \ "" \ "Nothing was changed." fi cc_step "Cloning ${CODECONNECT_REPO}…" GIT_TERMINAL_PROMPT=0 cc_run git -c credential.helper= \ clone --branch main "$CODECONNECT_REPO_URL" "$cc_stage_dir/src" || cc_die "could not clone $CODECONNECT_REPO." \ "" \ "Either the network is unavailable or the repository is not" \ "public. This message will not guess which." \ "" \ "Nothing was installed." # The clone moves out of staging; the (now empty) staging directory stays # registered so the exit trap still removes it. mv "$cc_stage_dir/src" "$src" cc_step "Building — this takes a few minutes…" # The repository's own installer does the build, the ad-hoc signing, the # inode replacement and the `source-checkout` record. Reimplementing any # of that here would be a second copy of logic that has already been paid # for in bugs. cc_run /bin/bash "$src/mac/install.sh" || cc_die "the CodeConnect installer did not complete." \ "" \ "The checkout is at $src and some binaries may have been" \ "replaced. Read the error above, fix it, and rerun:" \ "" \ " cd $src && ./mac/install.sh" cc_installed_from="source ($src)" } # ------------------------------------------------------------- placement # **The prefix is fixed at ~/.codeconnect/bin and this is not a style # preference.** `cc-hook`'s absolute path is written into the settings file # generated for each live session, and sessions deliberately outlive the # daemon. Installing the binaries anywhere else — or moving them during an # update — breaks the hooks of every session currently running. # # The rm-before-cp is the other half of the same care: macOS caches a # binary's code signature against its inode, and overwriting in place leaves # the cached signature describing different bytes. The kernel then SIGKILLs # every subsequent exec with no diagnostic at all. cc_place_binaries() { from="$1" bin="$CODECONNECT_PREFIX/bin" mkdir -p "$bin" cc_step "Installing to ${bin}…" for binary in codeconnect ccd cc-hook; do rm -f "$bin/$binary" cp "$from/$binary" "$bin/$binary" chmod 755 "$bin/$binary" # Never re-sign. An ad-hoc signature over a Developer ID one # destroys it, and with it the guarantee just verified. cc_run "$bin/$binary" --version >/dev/null 2>&1 || cc_die "the installed $binary does not run." \ "" \ "$bin may now hold a partially replaced set of binaries." \ "Rerun the installer once the cause is understood." done cc_say " codeconnect, ccd and cc-hook installed" # A daemon already under launchd keeps executing the binaries it started # with, so replacing files on disk without this leaves every visible sign # saying the new build is live while the old one is still serving. # Gated on the plist rather than on asking the daemon: a file test cannot # be refused, and the freshly signed inode can be refused exactly once. if [ -f "$HOME/Library/LaunchAgents/com.codeconnect.ccd.plist" ]; then cc_say " restarting the managed daemon…" cc_run "$bin/codeconnect" daemon restart >/dev/null 2>&1 || cc_warn "the daemon did not restart; run 'codeconnect daemon restart' yourself." fi } # ---------------------------------------------------------------- summary cc_summary() { bin="$CODECONNECT_PREFIX/bin" version="$(cc_run "$bin/codeconnect" --version 2>/dev/null || echo 'unknown')" cc_step "CodeConnect is installed." cc_say " $version" cc_say " from: $cc_installed_from" cc_say " at: $bin" cc_say "" # The PATH line is printed, never written. A child process cannot change # its parent's environment anyway, and editing someone's shell # configuration behind their back is not a thing this project does. case ":$PATH:" in *":$bin:"*) ;; *) cc_say "$bin is not on your PATH. For this terminal:" cc_say "" cc_say " export PATH=\"$bin:\$PATH\"" cc_say "" cc_say "To make it permanent, add that line to your shell profile" cc_say "yourself — this installer does not edit it for you." cc_say "" ;; esac cc_say "Next:" cc_say " codeconnect daemon install # run ccd under launchd" cc_say " codeconnect pair # QR code to pair your iPhone" cc_say " codeconnect claude # start a session here" cc_say "" cc_say "Nothing has been enabled for you. Those are yours to run." } # ------------------------------------------------------------------- main codeconnect_install_main() { set -eu umask 077 mode="release" cc_reinstall="no" for argument in "$@"; do case "$argument" in --source) mode="source" ;; --reinstall) cc_reinstall="yes" ;; --version) printf 'codeconnect-installer %s\n' "$CODECONNECT_INSTALLER_VERSION" return 0 ;; --help | -h) cc_usage return 0 ;; *) cc_say "unknown option: $argument" cc_usage return 2 ;; esac done cc_say "CodeConnect installer $CODECONNECT_INSTALLER_VERSION" # Everything that can refuse runs before anything that can write, and the # more specific refusal is ordered first: someone who already has # CodeConnect should be told that, not told the installer lacks a signing # identity. Same ranking CodeConnect itself uses when a checkout advisory # and a release advisory both apply. cc_preflight_common cc_preflight_platform cc_guard_existing if [ "$mode" = "release" ]; then cc_preflight_release fi # First write happens here. cc_prepare_prefix cc_acquire_lock cc_stage_dir="$(mktemp -d "$CODECONNECT_PREFIX/.install-stage.XXXXXX")" if [ "$mode" = "source" ]; then cc_install_source else cc_install_release fi cc_check_tmux cc_summary } codeconnect_install_main "$@"