Posts in category “Tips”

PI_CODING_AGENT_DIR points at the agent dir, not the `.pi` home

If you copy pi's default config to a custom dir and it silently "doesn't work at all" (no auth, no models), you probably pointed PI_CODING_AGENT_DIR one level too high.

The trap is that pi's default and its env override are asymmetric:

  • Default (no env var): the agent dir is ~/.pi/agent. Your auth.json/models.json live in ~/.pi/agent/.
  • Override: PI_CODING_AGENT_DIR is used verbatim as the agent dir — no /agent is appended.

So this looks right but isn't:

# WRONG: pi reads ~/.piz/auth.json (empty) and ~/.piz/models.json (missing)
PI_CODING_AGENT_DIR=~/.piz pi
# even though your real config is in ~/.piz/agent/

The fix is to point the variable at the agent leaf, mirroring the default layout:

PI_CODING_AGENT_DIR=~/.piz/agent pi

The relevant resolution (packages/coding-agent/src/config.ts):

export function getAgentDir(): string {
  const envDir = process.env[ENV_AGENT_DIR];
  if (envDir) return expandTildePath(envDir);        // used as-is
  return join(homedir(), CONFIG_DIR_NAME, "agent");  // default appends "/agent"
}

This feels weird next to .claude/.codex/.copilot, which keep their config flat in one dir and have no hidden subdir. But the name is the giveaway: it's PI_CODING_AGENT_DIR, not PI_HOME. It names the agent directory itself — the leaf, equivalent to ~/.pi/agent. The flat behavior is exactly what the name promises.

Once you read it that way the design makes sense: ~/.pi is just a home/container, and you can park several independent agent dirs under it (~/.pi/agent, ~/.pi/work, ~/.pi/personal) and switch between them with the env var.

For a rotating Claude gateway, key off the provider, not the token prefix

If you put Pi behind a local Anthropic gateway that rotates multiple Claude accounts, the interesting part is not the token format. The gateway may hand Pi a placeholder token like claude, while the real upstream auth happens inside the proxy. That means request shaping should be attached to the anthropic provider itself, not gated on whether the token looks like sk-ant-oat....

In pi-anthropic-auth, the fix was to make the transport wrapper unconditional for Anthropic requests. Pi already routes the built-in anthropic provider through the same registry transport, so once the request is in that lane, it can be shaped every time:

pi.registerProvider("anthropic", {
  oauth: anthropicOAuthOverride,
  api: "anthropic-messages",
  streamSimple: createAnthropicOAuthStreamSimple(builtinTransport.streamSimple),
});

That lets the proxy stay dumb and flexible. I ended up using ~/.pi/agent/models.json to point anthropic.baseUrl at the local gateway, and a placeholder OAuth record in ~/.pi/agent/auth.json:

{
  "providers": {
    "anthropic": {
      "baseUrl": "http://gateway-host:9949"
    }
  }
}
{
  "anthropic": {
    "type": "oauth",
    "access": "claude",
    "refresh": "",
    "expires": 1813218019502
  }
}

The gateway can then rotate across multiple Claude accounts behind the scenes, so one account hitting the 5-hour wall does not interrupt the session. Pi still sees a normal Anthropic provider, and the shaping layer still injects the Claude Code billing header and prompt normalization before the request leaves the machine.

Shell scripts can't `cd` for you — source them instead

A shell script always runs in a subshell. Any cd it executes vanishes when the script exits — the calling shell's working directory is unchanged.

The fix: source the script instead of executing it. Sourcing runs the script in the current shell, so cd sticks.

alias sw='. ~/bin/sw'

Two things to update in the script itself.

First, replace every exit with return. exit in a sourced script exits the entire shell, not just the script:

exit 1  →  return 1

Second, $0 in a sourced script is the shell binary (bash), not the script path. Use ${BASH_SOURCE[0]} wherever you need the script's own location:

source "$(dirname "${BASH_SOURCE[0]}")/git-common.sh"
script_name=$(basename "${BASH_SOURCE[0]}")

The one tradeoff: functions defined inside the script leak into the shell's namespace after it returns. For a personal utility this is usually fine.

Keep secrets out of your project files — even from AI agents

When you give an AI coding agent access to your workspace, any .env file in the project is fair game. The agent will read it, and the secrets end up in conversation history, logs, or tool call outputs. The fix isn't stricter prompting — it's structural: don't put the secrets there at all.

credential-gateway is a local proxy that solves this by injecting credentials at the network level. Your application connects to localhost:8080 (or localhost:3307 for MySQL, localhost:6380 for Redis) with no credentials in the connection string. The gateway holds the real credentials in ~/.config/credential-gateway/config.yaml — outside every project worktree — and injects them before forwarding each request upstream.

The config file lives at ~/.config/credential-gateway/config.yaml (or /etc/credential-gateway/config.yaml). The gateway refuses to start if the file is group- or world-readable, so a forgotten chmod doesn't quietly undo the whole point.

# example: OpenAI-compatible HTTP proxy
proxies:
  - type: http
    listen: :8080
    target: https://api.openai.com
    auth:
      header: Authorization
      value: "Bearer sk-..."

Your app then points at http://localhost:8080 and uses an empty or placeholder token. No .env, no secrets in the repo, nothing for an agent to read.

Supported today: HTTP (OpenAI-compatible APIs), MySQL, Redis. PostgreSQL support is in progress; Oracle basic support is planned.

If you're running AI agents against a codebase that talks to external APIs or databases, this pattern is worth adopting before a key leaks rather than after.

Stuck mid-task because your Claude account hit quota? Run multiple accounts behind a local proxy

If you use Claude Code heavily, you've probably hit the quota wall mid-conversation and had to either wait or manually switch to a second account — digging up the right ANTHROPIC_AUTH_TOKEN, setting the env var, restarting. It's enough friction to break flow completely.

agent-quota-gateway is a small Go reverse proxy you run locally on 127.0.0.1. You configure your Claude Code client to point at it instead of api.anthropic.com, and it manages a pool of your accounts behind the scenes. When one hits its quota limit, the gateway automatically switches to the next one and tells the client to retry — the mid-task context survives, the switch is transparent.

# point Claude Code at the local gateway
export ANTHROPIC_BASE_URL=http://127.0.0.1:9099

# the gateway reads credentials from env vars at startup
AQG_POOL_MAIN_BACKEND_ACCT1=sk-ant-...
AQG_POOL_MAIN_BACKEND_ACCT2=sk-ant-...

A nice side effect: because the gateway does credential substitution on every request, your real OAuth tokens never travel through Claude's context. If Claude Code ever leaks what it thinks is "your token" in a tool call or a log, it's just the pool name — the actual credential stays in the gateway process.

Anthropic's weekly limit means this can't be used to multiply quota — two accounts gives you two accounts' worth of budget, no more. It's purely a quality-of-life tool for people who have multiple subscriptions and want seamless failover instead of manual juggling. The gateway won't even bind to a non-loopback address unless you explicitly enable shared mode for a Tailscale-internal network.