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.

Got personal files to ignore? Use .git/info/exclude, not the shared .gitignore

When you want Git to ignore something that's yours — a local scratch dir, editor cruft, a personal plans/ folder, machine-local config — don't add it to the tracked .gitignore. That file is shared: a rule like plans/ or wiki-drafts/ forces your personal directory conventions onto everyone, and they don't have those dirs.

Put it in .git/info/exclude instead. Same syntax as .gitignore, but it's per-clone and never committed — so it's invisible to teammates and travels with nothing:

# .git/info/exclude
.claude/settings.local.json
.claude/plans/
.claude/wiki-drafts/

The rule of thumb: **.gitignore is for what the project should ignore (build output, node_modules/, *.user); .git/info/exclude is for what you happen to ignore on this machine.** If a teammate cloning fresh wouldn't also produce the file, it doesn't belong in the shared list.

One gotcha with worktrees: info/exclude lives in the common git dir, so it's shared across all linked worktrees of the same clone — but still not across separate clones. Find it with git rev-parse --git-common-dir if you're not sure where it is.

And if you ever need to start tracking something a blanket exclude was hiding (e.g. you had .claude excluded but now want .claude/skills/ committed), narrow the exclude line to the specific paths you still want ignored — don't delete it and shove replacement rules into the shared .gitignore.

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.