Simply @dotlink anywhere in ChatGPT — chat, space, or your dot to work with files on your machine, use local tools and projects you exposed — securely, from a single binary.
A single lightweight binary with an MCP server, OpenAI tunnel transport, permission engine, profiles, activity logging, and a full Bubblewrap sandbox on Linux — no UI, no third-party relay, guided setup.
- Let your ChatGPT Web and your dot work directly on your computer through controlled, fine-grained access: read and edit files, run Git, build, test, execute scripts, and use only the tools you explicitly expose.
- Give ChatGPT controlled access to several machines without installing the ChatGPT desktop app or provisioning SSH: run dotlink on each machine and expose only the projects, files, and capabilities you choose.
- Use local data without the upload/download loop: analyze files, build reports or slide decks, and work with artifacts directly from your machine.
- Use ChatGPT as a practical fallback when Codex usage is unavailable or exhausted: reconnect to the same project state and keep going.
When your machine is offline, keep planning against context already in ChatGPT; reconnect later and let ChatGPT re-read the live project before continuing.
For ChatGPT, dotlink opens an outbound OpenAI Secure MCP Tunnel directly from your machine to OpenAI. Your MCP server stays local: no public inbound port, no third-party relay.
Supports Claude.ai and other remote MCP clients over HTTP/ngrok, plus local MCP clients over stdio or loopback HTTP. Every transport uses the same local permission policy.
Security is local and opt-in: the launch directory is read-only by default; write, shell, network, extra paths, caches, and public HTTP are separate grants. Linux shell execution is Bubblewrap-sandboxed by default.
Linux x86_64/ARM64 or macOS Apple Silicon:
curl -fsSL https://raw.githubusercontent.com/abird-ai/dotlink/main/install.sh | shWindows x86_64/ARM64 PowerShell:
irm https://raw.githubusercontent.com/abird-ai/dotlink/main/install.ps1 | iexBoth installers download the latest published GitHub Release by default, select the correct architecture internally, verify its SHA-256, and require the downloaded executable to identify itself as the requested dotlink version before replacement. The command you run is always simply dotlink; architecture suffixes exist only on the release assets (dotlink.exe is the Windows file on disk). Re-run the same install command at any time to update; if the installed binary already matches the latest release, it is left unchanged.
Or build with Nix/Cargo; see Build below.
dotlink --setup
cd ~/src/my-project
dotlinkSetup is transactional: choose 0, none, or cancel to exit successfully without writing anything. Re-running --setup on an existing profile uses its current values as defaults; secret prompts show only [existing key], and blank input keeps the stored key.
Compatible older profile schemas are migrated forward automatically in memory. In particular, schema v9 loads directly as v10 because the v10 fields are additive/defaulted; the profile is written as the current schema the next time it is edited or saved. Newer schemas are never guessed, and older configs that need an explicit security decision—such as newly protected public ingress—fail with the specific setting that must be updated.
The launch directory is readable by default. Add capabilities only when needed:
dotlink --allow-rw
dotlink --allow-rw --allow-shell
dotlink --allow-rw --allow-shell --allow-networkdotlink profile list
dotlink profile show work
dotlink profile create work
dotlink profile edit work
dotlink profile delete work
dotlink profile allow work rw /shared
dotlink profile deny work read /secret
dotlink profile remove-allow work rw /shared
dotlink profile remove-deny work read /secret
dotlink profile enable work shell
dotlink profile enable work network
dotlink profile disable work networkUse default as the profile name to manage the unnamed default profile. Persisted settings can be toggled with profile enable/disable: openai, stdio, http, http-ephemeral-url, ngrok, ngrok-ephemeral-url, oauth, default-allow, shell, and network. Enabling a dependent setting automatically enables its prerequisite (network → shell, ngrok → http, oauth → http); disabling a parent safely disables its dependents. Enabling ngrok or oauth requires an owner OAuth credential; use dotlink profile edit <name> when one still needs to be configured.
Default runs are quiet. Add -v to see tool activity:
$ dotlink -v -p aw
dotlink v0.14.3
────────────────────────────────────────────────────────
• Profile aw
• Transports openai
• Tunnel tunnel_…
• Access read:2 write:1 deny-read:0 deny-write:0 caches:0 + shell
• Sandbox Bubblewrap
• Network enabled
• Logging TOOL
OpenAI Secure MCP Tunnel connecting…
Keys Ctrl-C: exit · Ctrl-R: restart · v: verbosity · V: full commands
✓ Connected — ready
[10:59:19.184] TOOL read → p="./.agents/plans/TODO.md"
[10:59:19.194] TOOL read ← ok 9ms
-v starts with TOOL activity; -vv adds REQ diagnostics. Interactive runs support v to cycle verbosity, V to toggle full command logging while TOOL logging is active, Ctrl+R to reload the runtime, and Ctrl+C to exit. stdio keeps stdin protocol-clean. File contents, binary payloads, Runtime keys, and raw request bodies are not intentionally logged.
Run setup once and select OpenAI Tunnel:
dotlink --setupThen start dotlink from the project you want ChatGPT to access:
cd ~/src/my-project
dotlinkCopy the printed tunnel_... ID and keep dotlink running.
- Open https://chatgpt.com/plugins.
- Select Add → Create MCP App.
- Name it abird dotlink and optionally add a short description.
- Under Connection, choose Tunnel.
- Paste the
tunnel_...ID printed by dotlink. - Accept the custom-MCP warning and create the MCP app.
- In a ChatGPT conversation, open the tools menu, enable abird dotlink, and ask normally.
After that, simply mention @dotlink in ChatGPT Web — in a normal chat, a Space, or your dot — whenever you want to work with the files and projects that machine has exposed.
For several computers, run dotlink on each machine and add each tunnel as its own MCP App. Name the connections by machine or project if useful. ChatGPT can then reach each machine under its own local permission policy, with no ChatGPT desktop app required on those machines.
If Create MCP App is unavailable, enable Developer mode under Settings → Security and login first.
The tools ChatGPT discovers follow dotlink's local permissions:
read-only ls, grep, read, bread
write + write, bwrite
read + write + edit, patch, bedit
shell + bash/powershell + durable job controls
Never paste a Runtime or Admin API key into ChatGPT. ChatGPT only needs the tunnel_... ID.
If the tunnel is not found, verify that dotlink shows ✓ Connected — ready, the tunnel belongs to the current ChatGPT workspace, and the Runtime API key has Tunnels Read + Use.
Detailed reference: docs/CHATGPT_PLUGIN.md.
Claude.ai custom connectors connect from Anthropic's cloud, so a loopback URL cannot be used directly. dotlink can publish its Streamable HTTP server through ngrok and automatically protects the public endpoint with OAuth.
Run setup, select HTTP, then enable the public ngrok endpoint:
dotlink --setupSetup asks for one owner password the first time OAuth is needed. The password itself is never stored; dotlink persists only an Argon2id hash in its private state directory.
Set your ngrok account token and start dotlink:
export NGROK_AUTHTOKEN='...'
dotlinkYou can also add HTTP/ngrok for one run:
dotlink --http --ngrokdotlink prints the public MCP resource and OAuth issuer:
✓ ngrok MCP: https://my-dotlink.ngrok.app/mcp
✓ OAuth: https://my-dotlink.ngrok.app (resource https://my-dotlink.ngrok.app/mcp)
For a durable connector, reserve an ngrok domain and persist it during setup or use:
dotlink --http --ngrok --ngrok-domain=my-dotlink.ngrok.appKeep the normal stable /mcp path for durable OAuth. An automatic ngrok hostname or --ngrok-ephemeral-url changes the OAuth resource identity when the URL changes, so the remote client must be re-linked.
- Open Customize → Connectors.
- Select + → Add custom connector.
- Name it abird dotlink.
- Paste the printed ngrok MCP URL.
- Connect it.
- When OAuth opens, review the client ID, redirect URI, resource, and scope, then enter your dotlink owner password and approve access.
The client receives short-lived bearer tokens plus rotating refresh tokens; reconnects do not require re-entering the owner password while the durable refresh grant remains valid.
For Team/Enterprise organizations, an owner may need to register the custom connector in organization connector settings before members can enable it.
In Claude, use the + menu in the composer, open Connectors, and enable abird dotlink. Claude sees only the MCP tools allowed by the running dotlink profile.
Any remote client that supports MCP Streamable HTTP + OAuth 2.1 can use the same endpoint. dotlink exposes Protected Resource Metadata, OAuth Authorization Server Metadata, authorization-code + PKCE S256, CIMD, DCR fallback, rotating refresh tokens, and revocation.
For a reverse proxy other than ngrok, protect local HTTP with OAuth and tell dotlink its canonical external origin:
dotlink --http --oauth --http-bind=127.0.0.1:3000 \
--public-url=https://mcp.example.comNever derive the OAuth issuer from proxy/Host headers; --public-url is the explicit trust boundary.
Public ngrok ingress is OAuth-protected by default. Direct non-loopback HTTP binds are also refused unless OAuth is enabled with an explicit HTTPS public origin. The only way to intentionally expose either form of public ingress without application-layer authentication is the explicit one-run escape hatch:
dotlink --http --ngrok --allow-public-no-auth
# or, for an intentionally unauthenticated LAN listener:
dotlink --http --http-bind=192.168.1.50:3000 --allow-public-no-authFor LAN use, replace the example address with the machine's actual interface address; wildcard binds do not identify a concrete public Host authority. This mode exposes every MCP capability granted to the process to anyone who can reach the endpoint and is not recommended.
For local MCP clients, no public tunnel is required.
Use stdio when the client launches MCP servers as child processes:
dotlink --stdio --allow-rw --allow-shellTypical MCP client configuration:
{
"mcpServers": {
"dotlink": {
"command": "dotlink",
"args": ["--stdio", "--allow-rw", "--allow-shell"]
}
}
}On Linux the shell is still Bubblewrap-sandboxed.
For local clients that support Streamable HTTP:
dotlink --http --allow-rw --allow-shellConnect to:
http://127.0.0.1:3000/mcp
This stays loopback-only unless you explicitly change --http-bind. Loopback HTTP is unauthenticated by default; add --oauth to protect it. When putting local HTTP behind a reverse proxy, also provide the canonical external origin with --public-url=https://....
One LocalMachine policy core serves every transport:
ChatGPT ── OpenAI Secure MCP Tunnel ─┐
Claude.ai / remote MCP ── HTTP/ngrok ├─ LocalMachine ─ policy-aware tools ─ host
Local MCP clients ── stdio/HTTP ─────┘
├─ Rust filesystem tools
└─ shell → Bubblewrap on Linux
Profiles persist transports, path grants/denies, shell/network policy, HTTP/ngrok options, and approved developer caches. Enabled profile transports start automatically; CLI flags add or suppress local transports for one run.
launch-directory read ON
launch-directory write OFF
extra filesystem paths OFF
shell OFF
shell network OFF
shared developer caches OFF
KVM / GPU devices OFF
public HTTP ingress OFF
unsandboxed host mode OFF
Capabilities are independent: --allow-read, --allow-write, --allow-rw, --allow-shell, and --allow-network. CLI grants compose with the selected profile; denies always win. --allow-rw=/ grants filesystem RW only. Full unsandboxed host authority is the explicit --unrestricted mode; the legacy --allow-all --no-sandbox pair remains equivalent.
ls / grep / read / bread require read
write / bwrite require write
edit / patch / bedit require read + write
Targets are canonicalized before checks; create targets canonicalize their nearest existing ancestor, preventing symlink escapes. The advertised MCP tool list is permission-aware, so unavailable write/shell tools are omitted rather than merely rejected later. Detailed tool, paging, mutation, replay, and platform invariants live in Architecture and Security.
With --allow-shell, Bash runs inside Bubblewrap by default. Read grants mount RO, read+write grants mount RW, denied paths are masked or downgraded, networking is isolated unless --allow-network is granted, credentials stay hidden, and only approved developer caches/Nix runtime paths are exposed. See docs/SECURITY.md for the full sandbox model.
Unsandboxed shell execution inherently has the OS user's filesystem and network authority, so dotlink exposes one explicit full-host escape hatch rather than several partial "dangerous" flags:
dotlink --unrestrictedThis grants:
filesystem read+write /
shell enabled
network enabled
sandbox disabled
For unrestricted filesystem access without removing the Linux sandbox, use the ordinary path model instead:
dotlink --allow-rw=/Then add --allow-shell and/or --allow-network separately if needed. On Linux, those capabilities remain Bubblewrap-sandboxed unless --unrestricted is explicitly selected.
This setup is shown only when OpenAI transport is enabled.
Setup asks for:
- a Runtime API key with Tunnels Read + Use;
- a tunnel:
- No Admin key in dotlink: create one at https://platform.openai.com/settings/organization/tunnels, then paste its existing
tunnel_...ID; - Create from dotlink: type
newand provide a one-time Admin API key with Tunnels Manage;
- No Admin key in dotlink: create one at https://platform.openai.com/settings/organization/tunnels, then paste its existing
- a ChatGPT Workspace ID or OpenAI Organization ID only when dotlink creates the tunnel.
The one-time Admin key is never persisted.
The Runtime key is stored separately from JSONC config and follows the selected profile:
~/.config/abird/dotlink/runtime.key
~/.config/abird/dotlink/runtime.work.key
DOTLINK_CONFIG may move the JSONC file, but Runtime keys always stay in dotlink's private XDG directory above. The active config and credential directory are control-plane state and are not exposed through dotlink's MCP/filesystem/shell surface.
When OpenAI transport is disabled for a profile, that profile does not require a runtime key.
Shell execution is durable: if a bash/powershell call cannot return its complete result inside the tunnel response window, the same execution continues behind a job/task handle instead of being restarted. The shell tool surface includes explicit background start, status, output paging, acknowledgement, and cancellation controls. See Architecture for the transport and replay model.
Transient OpenAI poll failures retry with backoff. Normal warnings stay concise; full transport diagnostics are available with -vv.
After 10 transient poll failures without an accepted poll, dotlink rebuilds the runtime from the current profile and policy. Repeated runtime restarts back off from 1 second to a 30-second cap and reset after a healthy connection; persistent tunnel/key rejection is given a short recovery window before operator action is required.
Useful locations:
- Tunnels: https://platform.openai.com/settings/organization/tunnels
- Runtime API keys: https://platform.openai.com/settings/organization/api-keys
- Admin API keys: https://platform.openai.com/settings/organization/admin-keys
- ChatGPT Workspace ID: https://chatgpt.com/admin
- OpenAI Organization ID: https://platform.openai.com/settings/organization/general
Logging is quiet by default:
default no TOOL / REQ activity
-v TOOL activity
-vv TOOL + transport/request REQ diagnostics
-q suppress TOOL activity (useful with -vv for REQ-only)
Example with -v:
[01:06:47.410] TOOL read → p="./README.md" limit=1
[01:06:47.411] TOOL read ← ok 1ms
With -vv, transport diagnostics are added:
[01:06:47.426] REQ stdio → initialize
[01:06:47.427] REQ stdio → tools/call
TOOL logs include bounded command previews plus complete path labels, methods, status, and latency; file contents, binary payloads, Runtime keys, and raw request bodies are not intentionally logged. Interactive V removes the command-preview cap while TOOL logging is active, so use it only when command text is safe to retain.
Color is automatic on interactive human-facing output, including --help. Override with --color=always, --color=never, or --color=auto. stdio keeps stdin protocol-clean and exposes only Ctrl+C.
The built-in help is the source of truth: dotlink --help, dotlink profile --help, and dotlink oauth --help.
SETUP & PROFILES
-S, --setup interactive setup/editor for the selected profile
-p, --profile <NAME> run/setup with config.<NAME>.jsonc
dotlink profile list
dotlink profile show <NAME>
dotlink profile create <NAME>
dotlink profile edit <NAME>
dotlink profile delete <NAME>
dotlink profile allow <NAME> <read|write|rw> <PATH>
dotlink profile remove-allow <NAME> <read|write|rw> <PATH>
dotlink profile deny <NAME> <read|write|rw> <PATH>
dotlink profile remove-deny <NAME> <read|write|rw> <PATH>
dotlink profile enable <NAME> <SETTING>
dotlink profile disable <NAME> <SETTING>
Persisted SETTING values:
openai, stdio, http, http-ephemeral-url, ngrok, ngrok-ephemeral-url,
oauth, default-allow, shell, network
OAUTH MANAGEMENT
dotlink oauth status [-p <NAME>]
dotlink oauth clients [-p <NAME>]
dotlink oauth revoke [-p <NAME>] <CLIENT_ID>
dotlink oauth revoke-all [-p <NAME>]
TRANSPORTS
--stdio add stdio MCP for this run
--no-stdio suppress profile stdio for this run
--http add HTTP MCP for this run
--no-http suppress profile HTTP + ngrok for this run
--http-bind <ADDR> override HTTP listen address
REMOTE HTTP & OAUTH
--ngrok publish effective HTTP through ngrok
--no-ngrok suppress profile ngrok for this run
--ngrok-domain <DOMAIN> use a stable/reserved ngrok hostname
--no-ngrok-domain ignore persisted ngrok domain for this run
--oauth protect local/reverse-proxied HTTP with OAuth
--no-oauth disable local HTTP OAuth for this run
--public-url <HTTPS-ORIGIN> canonical OAuth origin behind a reverse proxy
--allow-public-no-auth deliberately expose public HTTP/ngrok without OAuth
--ephemeral-url use ephemeral local HTTP + ngrok MCP paths
--http-ephemeral-url[=<BOOL>] override local HTTP path behavior
--ngrok-ephemeral-url[=<BOOL>] override ngrok path behavior
FILESYSTEM ACCESS
--allow-read[=<DIR>] add read; bare means launch directory
--allow-write[=<DIR>] bare: rw launch directory; with DIR: write-only
--allow-rw[=<DIR>] add rw; bare means launch directory
--no-default-allow remove implicit launch-directory read for this run
--deny-read[=<DIR>] deny read; bare means launch directory
--deny-write[=<DIR>] deny write; bare means launch directory
--deny-rw[=<DIR>] deny read+write; bare means launch directory
--deny <PATH> legacy synonym for --deny-rw=<PATH>
SHELL & NETWORK
--allow-shell enable Bash (Unix) / PowerShell (Windows)
--deny-shell disable shell; deny always wins
--allow-network network inside the Linux shell sandbox
--deny-network deny shell network; deny always wins
--allow-rw=/ filesystem rw / while keeping Linux sandboxing
--unrestricted full-host rw / + shell + network + no sandbox
--allow-all --no-sandbox legacy equivalent of --unrestricted
SHELL INTEGRATIONS (Linux Bubblewrap only; separately opted in)
--allow-nix-daemon share only the Nix daemon Unix socket
--allow-nix-config mount user's nix.conf read-only
--allow-ssh-agent share SSH agent socket; never private key files
--allow-git-config mount ~/.gitconfig and XDG Git config read-only
HARDWARE (Linux Bubblewrap; separate opt-in device access)
--allow-kvm pass /dev/kvm for accelerated VMs
--allow-gpu=amd pass AMD GPU render/KFD nodes and driver runtime
--allow-gpu=nvidia pass NVIDIA CUDA/render nodes and driver runtime
--allow-gpu may be repeated for both vendors
DEVELOPER CACHES (discovered paths; bare flag = ro, =rw enables writes)
--allow-cache-cargo-registry[=ro|rw] --allow-cache-cargo-git[=ro|rw]
--allow-cache-npm[=ro|rw] --allow-cache-pnpm[=ro|rw]
--allow-cache-yarn[=ro|rw] --allow-cache-pip[=ro|rw]
--allow-cache-uv[=ro|rw] --allow-cache-go-mod[=ro|rw]
--allow-cache-go-build[=ro|rw] --allow-cache-maven[=ro|rw]
--allow-cache-gradle[=ro|rw] --allow-cache-sccache[=ro|rw]
--allow-cache-ccache[=ro|rw]
OUTPUT & DIAGNOSTICS
--list-tools show tools exposed by the effective policy
--print-id print configured tunnel ID (no Runtime key needed)
-s, --serialize execute MCP tools one at a time (opt-in)
-q, --quiet suppress TOOL activity
-v, --verbose -v TOOL; -vv TOOL + REQ
--color <auto|always|never> control colors in help and human output
Linux Bubblewrap starts with a private home and no host SSH keys, Git config,
Nix config, or daemon socket. These optional shell-only grants expose just
the selected resource, not the whole user home or additional native MCP file
permissions. Interactive dotlink --setup discovers the available services
and caches, then asks before enabling each one; discovery never grants access.
dotlink --allow-rw --allow-shell \
--allow-nix-daemon --allow-nix-config \
--allow-git-config --allow-ssh-agent \
--allow-cache-cargo-registry=rw --allow-cache-cargo-git=rw- Nix: The daemon socket is available without
--allow-network, and dotlink selectsNIX_REMOTE=daemoninternally. Your XDG~/.config/nix/nix.confis mounted read-only into the private sandbox home if it exists. Ordinarynix build,nix run, andnix developcan then use your host Nix daemon, whose own fetch/build environment is outside Bubblewrap. It is a meaningful additional authority, not ordinary network access. - Git: Only
~/.gitconfigand$XDG_CONFIG_HOME/git/configare shared read-only when present. Theirinclude/includeIffiles are not implicitly shared. Beware plaintext tokens or credential helper settings inside Git configuration; only opt in for trusted shell workloads. - SSH agent: The detected agent (including GCR when no session
SSH_AUTH_SOCKexists) is mounted into the private shell, without mounting.sshor private key files. Agent access can request signatures and authentication using any key that agent offers; it is not limited to Git commits. Consider a dedicated signing-only agent orssh-add -cconfirmations. The sandbox also receives minimal read-only Unix account lookup files needed by OpenSSH.
The startup banner lists the host integrations actually mounted, without exposing socket locations or secret values.
For agent-backed Git commit signing, configure gpg.format=ssh,
commit.gpgsign=true, and a user.signingkey in the form
key::ssh-ed25519 AAAA... (the public key text). A signing-key path
under ~/.ssh is not automatically mounted.
All 13 cache families already supported by setup also have matching
--allow-cache-<kind>[=ro|rw] flags. Each finds the existing host cache
using the same discovery rules as onboarding. Bare flags share read-only;
=rw explicitly permits modifications to the host cache. An explicitly
requested cache whose directory does not exist is skipped with a warning
instead of failing startup, so declarative services do not break when a
cache has not been created yet. Cache grants
never expand filesystem MCP tool permissions and honor deny rules.
Systemd services have no reliable interactive SSH_AUTH_SOCK environment.
For services.dotlink.hostAccess.sshAgent = true, no socket path is needed
when GCR runs as the service user: dotlink discovers
/run/user/<uid>/gcr/ssh automatically. Detection prefers an explicit
services.dotlink.sshAgentSocket (a strict override), then a valid
SSH_AUTH_SOCK, $XDG_RUNTIME_DIR/gcr/ssh, and the effective user's
/run/user/<uid>/gcr/ssh. The module also includes Nix, Git, and OpenSSH
executables for enabled integrations. A system service does not start the GCR
agent: it must already be available at service startup. Restart dotlink after
the host agent/socket changes. See
.agents/docs/nixos-service.md.
Hardware devices are off by default, independently of network and file permissions. To run hardware-accelerated QEMU, GPU inference, CUDA/ROCm builds or Vulkan tests without turning off Bubblewrap:
dotlink --allow-shell --allow-rw --allow-kvm \
--allow-gpu=amd --allow-gpu=nvidiaThe vendor flags are repeatable. --setup discovers available KVM/AMD/NVIDIA
hardware and asks before enabling each capability. Only selected character
devices become accessible inside the sandbox. AMD GPU DRM nodes are verified
using the kernel PCI vendor ID (0x1002), NVIDIA DRM nodes using 0x10de;
unknown/Intel devices stay hidden. AMD also uses /dev/kfd when present;
NVIDIA uses the relevant /dev/nvidia* and capability nodes. Driver search
locations (/run/opengl-driver, Vulkan/OpenCL ICD paths, sysfs, and relevant
NVIDIA proc metadata) are shared read-only. NixOS graphics drivers must
already be configured on the host; dotlink does not bundle GPU toolchains.
--allow-kvm requires /dev/kvm on the host; VMs requiring TAP/TUN or GUI
displays may need separately authorized networking/display integration. Linux
user and group permissions still apply. GPU and KVM ioctls execute against
host kernel drivers, so treat these as significant additional permissions.
Requested hardware that is unavailable fails clearly rather than silently
falling back to software or allowing other devices. Changes to host driver or
device-node identities require restarting dotlink.
For NixOS services, configure hostAccess.kvm = true; and/or
hostAccess.gpu = [ "amd" "nvidia" ];. The module adds only the appropriate
service-level kvm, render and video supplementary groups, without
changing the Unix user's regular group membership. Provide optional GPU
utilities through extraPackages or your project's nix develop shell.
See .agents/docs/nixos-service.md.
A single OpenAI tunnel admits up to sixteen in-flight MCP requests; local HTTP and stdio follow client-directed concurrency. Tools execute concurrently by default. dotlink -s / --serialize opts into one call at a time within the process; it is an ordering preference, not a security boundary.
Native filesystem access is fail-closed and platform-specific: Linux uses descriptor-rooted openat2, macOS uses a pinned no-follow openat walk, and Windows uses pinned handles/reparse-point checks plus cross-process mutation coordination. See Architecture and Security for the exact invariants.
Restart dotlink (or press Ctrl+R in an interactive run) so the new profile/policy is active, then open https://chatgpt.com/settings/plugins-settings, select abird dotlink, and click Refresh tools.
Refreshing is especially important when the exposed tool set changes — for example, adding/removing write or shell access. It is also a good habit after changing path permissions so ChatGPT's connection metadata is definitely current.
No. dotlink starts read-only on the launch directory and exposes only the capabilities you grant. Write, shell, network, extra paths, caches, and unsandboxed access are separate opt-ins; deny rules always win.
No. Once connected, ChatGPT can read permitted local files directly through dotlink. That is useful for code, data analysis, reports, slide decks, and other local artifacts without repeatedly uploading/downloading files.
No. The ChatGPT path uses the outbound OpenAI Secure MCP Tunnel. Your MCP server stays local; no public inbound port or third-party relay is required.
Yes. Use named profiles such as dotlink -p work and manage them with dotlink profile .... Each profile can keep its own transports, permissions, shell/network settings, caches, and OpenAI tunnel state.
Not until the runtime reloads it. Restart dotlink or press Ctrl+R during an interactive run. If the ChatGPT-visible tool surface changed, also use Refresh tools in ChatGPT afterward.
Yes. Claude.ai and other remote MCP clients can use the HTTP/ngrok transport; local clients can use stdio or loopback Streamable HTTP. All transports share the same local permission policy.
Profiles live under ~/.config/abird/dotlink/ (or the equivalent XDG config directory). OpenAI Runtime API keys are stored separately from JSONC profiles and are never shown back in plaintext during setup.
OAuth state lives separately under $XDG_STATE_HOME/abird/dotlink/ (normally ~/.local/state/abird/dotlink/). dotlink stores only the Argon2id owner-password hash, approved DCR client metadata, and SHA-256 hashes of refresh tokens there; plaintext owner passwords, access tokens, and authorization codes are not persisted.
Use dotlink oauth clients -p <profile> to inspect approved DCR clients, then dotlink oauth revoke -p <profile> <client-id>. dotlink oauth revoke-all -p <profile> revokes all persisted refresh grants. Restart dotlink or press Ctrl+R if you also want all currently issued short-lived access tokens invalidated immediately.
NixOS users can enable dotlink as a systemd service directly from this flake. By default it reuses the selected user's existing profile, saved Runtime key, and OAuth state—no duplicated setup or generated configuration required:
# Add inputs.dotlink.nixosModules.default to your NixOS modules.
services.dotlink = {
enable = true;
user = "alice";
workingDirectory = "/home/alice/projects";
};The explicit working directory determines what dotlink's default "." read grant means. Use profile = "work"; to select an existing named profile. The default user is a dedicated dotlink system account; set user to your normal account to reuse its saved profile. Since this is a system service, custom shell-session XDG environment variables must be provided through services.dotlink.environment.
For declarative configuration, set services.dotlink.settings using the existing dotlink JSONC schema. It is generated as public Nix-store JSON (never put secrets in it). Leave settings unset to use the user's existing config. Optionally override only a credential via systemd LoadCredential:
services.dotlink.credentials.runtimeApiKeyFile =
config.sops.secrets.dotlink-runtime.path;Omitting this option retains the user's saved Runtime key. credentials.ngrokAuthTokenFile works similarly. There are also instances.<name> for independent named services, extraPackages for shell tools, and logging.verbosity = "lifecycle" | "tools" | "requests" (default: lifecycle). The service disables stdio MCP, gracefully handles SIGTERM, and logs to journald:
systemctl status dotlink
journalctl -u dotlink -fSee NixOS service setup, profiles, multi-instance operation, logging and security for the full option and lifecycle details.
Nix/Crane native workflow:
nix build
nix run .
nix flake check
nix developInside nix develop:
cargo fmt --check
alejandra --check flake.nix nix
markdownlint-cli2
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-features
cargo build --release --all-featuresnix flake check runs the package build, Rust tests, rustfmt, Clippy (warnings
denied), Markdown linting, Alejandra formatting checks, and Linux NixOS module
checks. Each check is independently selectable, for example
nix build .#checks.x86_64-linux.markdownlint. nix fmt formats Nix files with
Alejandra. Markdown linting includes .agents/ documentation; its configuration
allows soft-wrapped prose and repeated headings in separate changelog sections.
CI runs these checks before building release artifacts.
Build the complete reproducible release bundle from an x86_64 Linux host:
nix build .#release-all
# or copy the bundle into ./dist:
./scripts/build-release-artifacts.shThe bundle contains static Linux x86_64/ARM64 binaries, Windows x86_64/ARM64, and macOS ARM64. The static Linux builds run across NixOS, Debian, and other compatible distributions. Individual Nix outputs are available as dist-linux-*, dist-windows-*, and dist-macos-aarch64.
To publish a GitHub Release, push a tag matching the Cargo version, such as v0.14.3. CI rebuilds the Nix release graph and uploads each binary plus its .sha256 sidecar as an individual release asset; the Actions ZIP bundle is not published as the release.
Cross-build outputs, stable filenames, installer overrides, and platform release details: .agents/docs/release-install.md.
The filesystem MCP tools (read, write, edit, ls, binary tools) still enforce dotlink's allow/deny policy on macOS and Windows.
The shell is different: Bubblewrap is Linux-specific, so native macOS and Windows do not currently have an equivalent dotlink shell sandbox. To enable shell execution natively on those platforms, use the explicit full-host mode:
dotlink --unrestrictedThat intentionally removes dotlink's shell isolation and gives the child shell the OS user's filesystem/network authority. Use it only when that is what you want.
For Windows development, the recommended secure shell workflow is to run dotlink inside WSL2 and use the normal Linux Bubblewrap sandbox there:
# inside WSL2
dotlink --allow-rw --allow-shellThat preserves the same Linux permission/mount/network model described above instead of exposing an unrestricted native PowerShell shell.
Native Windows can still use the Rust filesystem tools with allow/deny enforcement without enabling PowerShell.
macOS can use the Rust filesystem tools with the same allow/deny policy, plus stdio/HTTP/OpenAI transports. Because Bubblewrap is unavailable, shell execution requires --unrestricted (or the legacy --allow-all --no-sandbox pair) until a macOS-native sandbox backend is added.
Apple Silicon release binaries are cross-built reproducibly from the same x86_64 Linux Nix release graph using a pinned Apple SDK and LLVM's Mach-O linker. Intel macOS is not currently published; build it from source with Cargo if needed.
We build dotlink in the open and would love your ideas, architectural challenges, research, and focused improvements. See CONTRIBUTING.md for the best ways to take part.
Human-centric design. AI-centric implementation. dotlink is built with extensive AI assistance for research, code, tests, and documentation. We believe the best software comes from human-led design and AI-assisted implementation: humans own the architecture, priorities, and acceptance criteria, while AI helps implement, test, and iterate. We don't hand direction to autonomous models or claim every generated line is manually reviewed — correctness rests on explicit designs, reproducible tests, and continuous validation.
MIT © 2026 Prasanna Loganathar and dotlink contributors. Third-party libraries and assets retain their respective licenses.