Skip to main content

tailctl — per-identity Tailscale networking for parallel sessions on a single Mac

tailctl

Per-identity Tailscale networking for parallel sessions on a single Mac.

tailctl gives each process (e.g. a Claude Code session in its own Ghostty window) its own Tailscale identity — a private, headless userspace tailscaled with a per-identity SOCKS5/HTTP proxy and localhost port-forwards — instead of switching the machine-global Tailscale account. Many identities run at once, and the GUI Tailscale app (the browser's default tailnet) is never touched. No FIFO queue, no shared network, no flapping.

Agent traffic is opted in per command via proxy env vars (tailctl run) or stable localhost forwards for native-TCP clients (ClickHouse, Postgres, …).

No always-on daemon beyond the per-identity tailscaled instances. Single-laptop scope.

Status

Working prototype — macOS-only, single-laptop scope.

Install

Requires macOS, Python 3.11+, and the Homebrew Tailscale daemon (see Requirements).

brew install drycodeworks/tap/tailctl

Homebrew pulls in the tailscale formula for you. From PyPI instead, install the daemon yourself:

brew install tailscale
pipx install tailctl        # or: pip install tailctl

Or from source:

brew install tailscale
git clone https://github.com/DRYCodeWorks/tailctl && cd tailctl
python3 -m venv .venv && .venv/bin/pip install -e .
ln -sf "$PWD/.venv/bin/tailctl" ~/.local/bin/tailctl   # put it on PATH

Quick view

tailctl init                       # scaffold ~/.tailctl/profiles.yaml
tailctl doctor                     # validate config + probe daemon + verify accounts
tailctl bootstrap <name> --tailnet <t> [--exit-node <e>] \
  [--auth-key-env ENV] [--auth-key-ssm NAME [--auth-key-ssm-profile P] [--auth-key-ssm-region R]] \
  [--port-forward svc=host:port]   # onboard a new tailnet with explicit auth sources
tailctl up acme-dev              # spawn a userspace tailscaled for the profile
                                   #   (first time: prints a one-time auth URL)
tailctl run acme-dev -- curl http://service.internal   # route a command via that identity
tailctl run acme-dev -- clickhouse-client --host 127.0.0.1 --port $CLICKHOUSE_ADDR
tailctl ps                         # list running instances + their proxy ports/forwards
tailctl shields [--fix]            # confirm every instance refuses inbound (exit 5 if not)
tailctl down acme-dev            # release (refcount--); stops the daemon at zero

A userspace node delivers inbound tailnet connections to this host's 127.0.0.1, so tailctl keeps shields up on every instance: up and run read the pref back and raise it, and refuse to hand out an instance they can't confirm.

run sets ALL_PROXY/HTTPS_PROXY/HTTP_PROXY (+ NO_PROXY) so proxy-aware clients route through the identity, and exports <SERVICE>_ADDR=<listen_host>:<port> for each configured port_forward so native-TCP clients connect to localhost.

The proxies take no credentials, so anything that can reach their port can use the identity. They bind listen_host (default 127.0.0.1). On a machine that runs containers, set it to a loopback alias: OrbStack forwards host.docker.internal to 127.0.0.1 and ::1 (but not to other lo0 addresses), so every container could otherwise tunnel into the tailnet. The alias must already be on lo0 (sudo ifconfig lo0 alias 127.84.67.1 255.255.255.255, run at boot). If it is missing, up and run refuse to start rather than fall back to 127.0.0.1. An instance spawned before listen_host changed keeps its old address until its holders exit, and every claim on it prints a warning.

Port forwards are for protocols that are not HTTP (SQL Server, ClickHouse native, Postgres). A forward relays raw bytes, so a web page that rebinds a hostname to its address could talk through it. Reach HTTP services through run's proxy variables instead.

profiles.yaml

default: acme-dev
tailscaled_binary: /opt/homebrew/opt/tailscale/bin/tailscaled
listen_host: 127.84.67.1         # optional loopback alias; default 127.0.0.1
profiles:
  acme-dev:
    account_id: a2c4
    tailnet: acme.example.ts.net
    exit_node: tailscale-subnet-router-development
    accept_routes: true
    auth_key_env: ACME_TS_AUTHKEY   # optional headless-wrapper short circuit
    auth_key_ssm: /acme/tailscale/dev_authkey  # optional SSM fallback
    auth_key_ssm_profile: acme-secrets         # optional AWS CLI profile for that read
    auth_key_ssm_region: us-west-2             # optional; default is the profile's region
    port_forwards:
      - service: clickhouse        # exported as CLICKHOUSE_ADDR
        remote_host: 100.64.0.10
        remote_port: 9000

Authentication

Each profile is a distinct device on its tailnet, authenticated once—the login persists in the profile's statedir and is reused on every later up/run (teardown never logs out). Authentication order:

  1. auth_key_env, when a headless wrapper injects it.
  2. The auth_key_ssm parameter, read with the AWS CLI as auth_key_ssm_profile in auth_key_ssm_region. Without those, the read uses your default AWS profile and its configured region.
  3. Interactive login URL when neither source resolves.

Both sources are optional and independent. The environment always wins, so a headless wrapper that injects the key never attempts an SSO-backed read. SSM subprocesses receive no inherited AWS_* variables, so an ambient profile, credential or endpoint override can't redirect the read.

Profiles that share an account but differ by exit node (e.g. Acme dev/staging/prod) are separate profiles → separate nodes → one auth each.

Requirements

  • macOS
  • Homebrew Tailscale for the userspace daemon: brew install tailscale (coexists with the GUI Tailscale.app, which keeps the browser's default tailnet)
  • Python 3.11+
  • Each profile's Tailscale account authenticated once per profile on first up

License

MIT

Metadata

Release files for tailctl 0.2.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for tailctl 0.2.1
File Size Uploaded Provenance
tailctl-0.2.1.tar.gz 61.7 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for tailctl 0.2.1
File Interpreter ABI Platform
tailctl-0.2.1-py3-none-any.whl Python 3 none any Details

Total release size: 107.2 kB

Release files / tailctl-0.2.1.tar.gz

Download URL tailctl-0.2.1.tar.gz
Size 61.7 kB
Tags Source
SHA-256 checksum
How to use checksums
353dc7413b8ce56e16a3e566fdd2ea6b132db5cca736dc9b2a90d0461411b8c3
BLAKE2b-256 checksum
How to use checksums
24882459c0d2342ed8bfe19f13e464eeff0f4229c265c5870b678bae42720c34
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.

Transparency log

Release files / tailctl-0.2.1-py3-none-any.whl

Download URL tailctl-0.2.1-py3-none-any.whl
Size 45.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
27558ee8857d1677263e391ea0b68e1d325ea00c2690d79eff17161f5ec218ee
BLAKE2b-256 checksum
How to use checksums
c997629b9b821bbc11906d53fa0aca9ec37183eeeac021fe2d037bd45d4ec99c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.

Transparency log

Security for tailctl 0.2.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for tailctl-0.2.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page