Skip to content

Remote access

CodeNavi serves plain HTTP on localhost:8765. Any client that is not on the same machine needs two things:

  1. the auth token enabled (required — never expose an open server), and
  2. a way to reach the server: plain HTTP over a network you trust (the Same LAN recipe — a developer's laptop, a second desktop, a team box), or an HTTPS URL (a tunnel, or your own reverse proxy) for the clients that require one — Claude Desktop's Connectors UI, claude.ai on the web or your phone, a hosted agent.

This page is the shared recipe. Client guides (Claude Desktop connectors, Antigravity on Windows, team server) link back here for these steps and add only their own client config.

Pick the LAN recipe unless a client forces HTTPS. An SSH tunnel (ssh -L 8765:localhost:8765 you@host) also works when the server must stay loopback-only — but never land it on port 8765 of a Windows box that already port-proxies that port to a WSL2 CodeNavi: the two collide and the client silently talks to the wrong server.

Step 0 — Enable the auth token (required)

A public URL publishes your endpoint to the internet. Do not skip this step.

In deploy/.env:

CODENAVI_AUTH_TOKEN=<generate-a-long-random-string>

Generate one with: python3 -c "import secrets; print(secrets.token_urlsafe(32))"

Restart the container:

cd ~/codenavi/deploy && docker compose up -d

Verify the token is enforced:

export TOKEN=$(grep '^CODENAVI_AUTH_TOKEN=' deploy/.env | cut -d= -f2-)
curl -i http://localhost:8765/mcp        # expect 401
curl -i -H "Authorization: Bearer $TOKEN" http://localhost:8765/mcp  # expect 200/SSE

You can change the token, but you cannot silently drop it: if the server is bound to a non-loopback address (any host-networked or 0.0.0.0 deploy) with no token, it refuses to start rather than exposing every tool and indexed codebase to the network. A loopback-only bind with no token is fine. To run tokenless on a trusted single-user host anyway, acknowledge it with CODENAVI_ALLOW_NO_AUTH=1.

Same LAN — plain HTTP (no tunnel)

The simplest split: the server on one box, your terminal or IDE on another, both on a network you trust. Plain HTTP with the bearer token is enough — the token is what protects the endpoint, and nothing leaves the LAN.

On the server

  1. Token on (Step 0).
  2. Publish the port beyond loopback — in .env:
CODENAVI_HOST=0.0.0.0

then docker compose up -d from the install directory. This is the host-side publish interface of ports-mode installs (CPU, Docker Desktop); host-network installs (the NVIDIA and ROCm overlays) already listen on every interface and ignore it — set it anyway, it is harmless. 3. Open 8765/tcp in the host firewall (sudo ufw allow 8765/tcp, or sudo firewall-cmd --add-port=8765/tcp --permanent && sudo firewall-cmd --reload, or the equivalent Windows Defender inbound rule). 4. Note the address clients will use — hostname -I on Linux (an address like 192.168.x.x).

From the client

  1. Verify reachability with the token:
curl -s -H "Authorization: Bearer <token>" http://<server-ip>:8765/health

A full body with codebases = reachable and authenticated. {"auth_required": true} alone = reachable, wrong or missing token. Connection refused = server steps 2–3. 2. Open the wizard at the server's address, from the client — http://<server-ip>:8765/connect. The rendered config follows the address in the browser bar, so every snippet already reads http://<server-ip>:8765/mcp. Paste the token there once and use your client's snippet as-is. 3. Claude Code specifically: - With the bundled plugin — the server hands it out itself; nothing to copy from the package dir:

 ```bash
 claude plugin marketplace add http://<server-ip>:8765/plugins/claude.git
 claude plugin install codenavi@codenavi
 ```

 No token needed for `/plugins/*` — the plugins are MIT and carry nothing secret. Already added the marketplace from a local `./plugins/claude` path? `claude plugin marketplace remove codenavi` first; the names collide. From here on `claude plugin marketplace update codenavi` (and Claude Code's background auto-update) pulls whatever the server runs, so the plugin follows every server upgrade. Then tell the plugin where the server is — under `"env"` in `~/.claude/settings.json`:

 ```json
 { "env": { "CODENAVI_HOST": "<server-ip>", "CODENAVI_PORT": "8765", "CODENAVI_AUTH_TOKEN": "<token>" } }
 ```

 Restart Claude Code. The plugin's MCP entry (`http://${CODENAVI_HOST:-localhost}:${CODENAVI_PORT:-8765}/mcp`, plugin 0.5.7+), the statusline badge and the SessionStart hook all read these three variables — one block wires all of them, on every OS (a shell `export` dies with the shell, and Windows-native Claude Code has no launching shell). `/codenavi:doctor` asks for the address when `localhost` doesn't answer and writes the block for you.
  • Without the plugin — register the server at user scope, then confirm:

    claude mcp add -s user --transport http codenavi http://<server-ip>:8765/mcp \
      --header "Authorization: Bearer <token>"
    claude mcp list      # codenavi: http://<server-ip>:8765/mcp (HTTP) - ✓ Connected
    

    Don't hand-write ~/.claude/mcp.json — Claude Code never reads that file. claude mcp list is the only trustworthy check of what is registered. - Windows client that also runs a WSL2 CodeNavi? Its localhost:8765 is probably a netsh port proxy to the WSL instance — a different server. Anything left at the localhost default (the badge, the hook, an old .mcp.json) talks to it, gets a valid-looking /health and a rejected token, and the statusline shows the 🔒 ✦ lock. Set CODENAVI_HOST explicitly, and never tunnel onto 8765 there. 4. Other clients: the wizard snippet is complete. The Codex and Antigravity bundles are served the same way, and their connect-codenavi.sh takes the server address:

git clone http://<server-ip>:8765/plugins/codex.git && cd codex      # or antigravity.git
export CODENAVI_AUTH_TOKEN=<token>
./connect-codenavi.sh --host <server-ip> --port 8765

No git on the client? curl -O http://<server-ip>:8765/plugins/codex.zip unpacks to codenavi-plugin-codex/; run the script from there. CODENAVI_HOST in the environment works instead of --host.

CODENAVI_HOST means two things. On the server it is the interface Docker publishes on (0.0.0.0 = every interface). On the client — plugin, statusline, hook — it is the server's address (<server-ip>). Don't copy one into the other.

Tailscale Funnel gives you a stable https://<host>.ts.net URL backed by Let's Encrypt, with no inbound firewall changes.

1. Install Tailscale on the CodeNavi host

curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up

2. Enable HTTPS and Funnel for your tailnet

In the Tailscale admin console (https://login.tailscale.com/admin/dns): - Enable MagicDNS - Enable HTTPS Certificates - Under Access controls, add the funnel attribute to the node (or to a tag the node belongs to). Example ACL fragment:

{
  "nodeAttrs": [
    { "target": ["tag:codenavi"], "attr": ["funnel"] }
  ]
}

3. Make sure CodeNavi binds to a local interface Funnel can reach

Tailscale Funnel forwards to a local port on the host. CodeNavi defaults to 127.0.0.1:8765, which is fine for Funnel since both run on the same machine — but if you've changed CODENAVI_HOST, leave it as 127.0.0.1 or 0.0.0.0. Verify:

ss -tlnp | grep 8765
curl -s http://localhost:8765/health | python3 -m json.tool

4. Start the Funnel

sudo tailscale funnel --bg 8765

This serves http://localhost:8765 at https://<host>.<tailnet>.ts.net/ on ports 443/8443/10000. Confirm with:

tailscale funnel status

You should see something like:

https://codenavi-host.tail1234.ts.net (Funnel on)
|-- / proxy http://127.0.0.1:8765

Option B — Cloudflare Tunnel

If you don't want Tailscale, cloudflared works the same way:

# Install cloudflared, then:
cloudflared tunnel --url http://localhost:8765

This prints a https://<random>.trycloudflare.com URL. The URL changes every restart unless you create a named tunnel — see Cloudflare's docs.

Set CODENAVI_AUTH_TOKEN first, same as Option A. The trycloudflare URL is publicly indexable.

Option C — Your own reverse proxy (Caddy / nginx)

If the CodeNavi host already has a public DNS name and reachable ports, you don't need a tunnel — put a reverse proxy with TLS in front.

Caddy (easiest — auto-HTTPS)

# Install: https://caddyserver.com/docs/install
# Caddyfile:
codenavi.example.com {
    reverse_proxy localhost:8765
}

Caddy auto-provisions Let's Encrypt certificates. Then connect your MCP client to https://codenavi.example.com/mcp.

nginx

server {
    listen 443 ssl;
    server_name codenavi.example.com;

    ssl_certificate     /etc/letsencrypt/live/codenavi.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/codenavi.example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8765;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_buffering off;
        proxy_read_timeout 900s;
    }
}

The SSE streaming settings matter: proxy_buffering off and the Upgrade/Connection headers keep MCP's streamed responses flowing through the proxy, and proxy_read_timeout outlives long tool calls (nginx's 60 s default cuts off a reindex). The proxy-only path — HTTPS without touching the install at all, plus the firewall and remote-test checklist — is on its own page: HTTPS in front of CodeNavi.

Tell CodeNavi its public URL

Whichever option you chose, add the resulting origin to deploy/.env and restart:

CODENAVI_PUBLIC_URL=https://codenavi-host.tail1234.ts.net   # scheme + host, no path

Without it the server still works — every client above sends the bearer token directly — but the self-describing URLs it emits (/health's mcp_endpoint, the OAuth resource metadata a client is pointed at after a 401) read http://localhost:8765, so a client with a missing or wrong token gets sent to its own machine instead of a clear auth error. It does not change what CodeNavi listens on; TLS stays with the proxy or tunnel.

Using the URL

Everything the server exposes locally is now at your HTTPS base URL, same paths:

Endpoint Purpose
https://<your-url>/mcp MCP, all codebases (pass codebase= per call)
https://<your-url>/mcp/<codebase> MCP, pinned to one codebase
https://<your-url>/health health check (handy for testing the tunnel)
https://<your-url>/.well-known/agent-card.json agent self-discovery card

Clients send the token as a header: Authorization: Bearer <your-token>.

On a multi-codebase server, clients that keep one URL per connection (e.g. Claude Desktop connectors) want the scoped form — one entry per codebase. See setup.md for how scoped endpoints behave.

Troubleshooting

Client can't connect at all

  1. Check the proxy is up: tailscale funnel status, cloudflared tunnel info, or (Option C) confirm your reverse-proxy service is running and DNS/port 443 resolve
  2. Test the URL from outside the host: curl -i -H "Authorization: Bearer $TOKEN" https://<your-url>/health
  3. Check docker compose logs (from your install directory) for auth rejections

401 Unauthorized

The token the client sends doesn't match CODENAVI_AUTH_TOKEN in deploy/.env. Re-paste it carefully — no extra whitespace, include the Bearer prefix when the client asks for a raw header value.

Tools appear but every call times out

The tunnel forwards to 127.0.0.1:8765 on the host. If CodeNavi is bound to a different interface or port, fix CODENAVI_HOST / CODENAVI_PORT in deploy/.env and restart.

Client keeps "reconnecting" between calls

CodeNavi runs stateless_http=True, which causes cosmetic SSE reconnect notices in some clients after each tool call. Tool results still return correctly. This is a known behavior, not a bug.

Security notes

  • Never expose CodeNavi publicly without an auth token. The MCP endpoint can read source code, run searches, and (if enabled) trigger reindex.
  • The auth token is shared-secret only. For per-user auth, put an OAuth proxy (e.g., oauth2-proxy) in front — out of scope for this guide.
  • Funnel traffic is end-to-end TLS via Let's Encrypt. Cloudflare Tunnel terminates TLS at Cloudflare's edge.
  • Disable the tunnel when you're not using it (sudo tailscale funnel --bg off) if you want to minimize the exposure window.