Skip to content

Set up the project ​

Follow this guide on the macOS computer that will host the proxy. Keep it awake while clients need access. You do not need a Cloudflare account to run the proxy; Cloudflare is only used to publish the website.

1. Install the prerequisites ​

Install Git and the command-line developer tools, Docker Desktop or another Docker runtime with Compose v2, the 1Password CLI, and Go 1.27 or newer (the version declared in the Go modules).

The scripts also use Bash, curl, security, and htpasswd. macOS supplies these commands. If htpasswd is missing, install Apache's password utility. portless is optional for the LAN URL. Install Claude Code and/or Codex only if you want those clients. Node.js 22.12+ and npm are needed for the website; a current Node.js LTS release is recommended. Bun is needed only to rebuild the management panel.

Check the tools before proceeding:

bash
git --version
docker --version
docker compose version
op --version
go version
command -v bash curl security htpasswd

Start Docker, then confirm the daemon responds:

bash
docker info

2. Clone and inspect the repository ​

bash
git clone https://github.com/oullin/cli-proxy-api.git
cd cli-proxy-api
make

make lists the available commands without starting services. Read config.yaml and local/compose.yaml: this is a deployment wrapper around an upstream proxy, with the image pinned in Compose.

3. Create the 1Password item ​

Enable Integrate with 1Password CLI in the 1Password desktop app's developer settings and unlock the app. If you do not use desktop integration, follow 1Password's CLI sign-in instructions.

The defaults look for an item named cli-proxy-api in a vault named Private, on my.1password.com. Create a secure item with these two exact field names:

FieldValue and use
claude-api-keyA new, long random key used by all proxy clients, including Codex
management-passwordA separate long random password for the panel and management API

Generate two different values with 1Password's password generator. The historical field name claude-api-key does not restrict it to Claude. Do not paste either value into tracked files, screenshots, issues, or terminal transcripts.

If your account, vault, or item has a different name, export the settings in the terminal that runs the Make commands:

bash
export OP_ACCOUNT='my.1password.com'
export OP_VAULT='Private'
export OP_ITEM_NAME='cli-proxy-api'

Replace these examples with your own values. These variables must also be available to clients that run hara-key directly. A shell profile can hold the names; store the credentials themselves only in 1Password. For clients launched outside a terminal, you can instead save default names in a private ~/.cli-proxy-api/credentials.env file (permissions 600):

bash
: "${OP_ACCOUNT:=my.1password.com}"
: "${OP_VAULT:=Private}"
: "${OP_ITEM_NAME:=cli-proxy-api}"

The helpers source this file before applying defaults. Keep this file outside the checkout, use your own vault and item names, and preserve the default-assignment syntax so explicit environment overrides still work. HARA_ENV_FILE can select a different private file.

Check access without printing the secret:

bash
op read "op://$OP_VAULT/$OP_ITEM_NAME/claude-api-key" >/dev/null
op read "op://$OP_VAULT/$OP_ITEM_NAME/management-password" >/dev/null

scripts/hara-key caches these fields in the macOS login Keychain for 30 days. make up uses op inject to render the client key and writes the management password as a bcrypt hash. Changing vault or item settings requires refreshing the cached keys with make ops keys.

4. Prepare Tailscale and optional LAN access ​

Create or use your own Tailscale account. In its DNS settings, enable MagicDNS and HTTPS certificates. No public port forwarding is required. On the first start, the Tailscale container provides a login link; you will authorize the new device in the next step.

Install portless if you want https://hara.local. The startup script starts it in LAN mode, which exposes every portless app on this computer to the local network. Binding HTTPS port 443 may ask for your macOS password. Read Network and TLS before enabling this on a shared network.

Without portless, the Docker proxy still runs at http://localhost:8317, but the final make status can fail its LAN check. Use URL=http://localhost:8317 for client and operations commands; this changes their target and does not disable the status command's separate LAN check.

5. Start the stack ​

bash
make up

Startup renders configuration, writes the private quota secret file, builds the quota image, starts the proxy/Tailscale/quota containers, waits for /healthz, and registers the optional LAN address. The initial image pull and Go build can take a few minutes.

The first status report may say that Tailscale is not signed in. Open the login link it prints, authorize the device, and run:

bash
make status

Use the tailnet address reported by your own installation. Container health and a valid client key can succeed before any provider account is connected. The accounts check remains incomplete until you add one.

6. Connect an upstream provider ​

Open http://localhost:8317/management.html on the host computer, or https://hara.local/management.html when portless is ready. Enter your management-password, then follow Connect providers.

Start with one account. Verify that it appears before adding more:

bash
make ops accounts URL=http://localhost:8317
make ops models URL=http://localhost:8317

7. Verify a real request ​

For a connected Claude account, pick a model returned by make ops models:

bash
make ops smoke URL=http://localhost:8317 MODEL=claude-haiku-4-5-20251001

This sends a real upstream request and consumes provider usage. If you connected a different provider, use its listed model or the OpenAI-compatible curl example.

Check WebSocket connectivity independently of provider access:

bash
make ops ws-smoke URL=http://localhost:8317

The command also checks loopback and the selected URL. With portless ready, omit the URL override to include the LAN URL. Supply TS_URL to check your tailnet as well.

Finally, follow Configure clients. A working management panel alone does not prove that a provider model or a client's transport works.

Where private state lives ​

By default, state is stored outside this checkout:

DirectoryContents
~/.cli-proxy-api/proxy/Rendered config, OAuth credentials, cooldown state, and logs
~/.cli-proxy-api/tailscale/Tailscale device identity
~/.cli-proxy-api/quota/Management password file for the quota container
~/.cli-proxy-api/backups/Optional private client configuration backups

Use an absolute LOCAL_DIR to move runtime state, and keep the same setting on later commands. Never set it inside the repository. make down stops containers but keeps this state for the next start.

hara · built on CLIProxyAPI