The Pithy config directory

Nothing secret Pithy holds is in your repository. There is one directory per machine that holds it instead, and this page is what is in there and why it is not in your project.

Where it is

The reference has every file it holds. The two that matter most are secrets and the port registry worktree development depends on.

Resolved in this order:

  1. $PITHY_CONFIG_DIR, if set
  2. %APPDATA%\pithy on Windows
  3. $XDG_CONFIG_HOME/pithy
  4. ~/.config/pithy

Created 0700, with 0600 files.

pithy doctor prints the resolved paths on every run, because nothing in your project mentions them. A path outside your checkout is one no editor’s file tree reaches and no ls in the project finds, so a diagnostic that does not name it is a diagnostic that leaves you searching.

Why machine-level rather than per-repo

The port registry is the story that settled it.

dev-ports.json used to sit at the repo root. So every project on a machine kept its own registry, every one of them started empty, and every one handed out the first block — which meant two projects on their default branch pinned the same twenty ports, and the second refused to start with nothing able to say who had taken them.

A registry that is per checkout cannot answer a question about the machine. So it moved, and once it had, the rest followed for the same reason: credentials are a property of the account, not of a project, and one copy of the same token per project makes rotation an N-place edit.

What is in it

PathWhat it is
dev-ports.jsonThe port-block registry for the whole machine, keyed by checkout then by branch
cloudflare.jsonAccount credentials, written by pithy init
cloudflare.<accountName>.jsonThe same, for a project that names an account. One machine, several companies, one file each
state.jsonThe update notifier’s cache
<project>/secrets.jsoncYour dev secret values. The source every generated .dev.vars is built from
<project>/dev.jsonMachine-local dev vars no registry declares, and the dev-login opt-in
<project>/tokens.jsonMinted CI tokens — a handoff to you, and never read back

The last three are per project, keyed by the project name — which is why several commands refuse to guess that name.

What this buys you

Several projects and several worktrees of each, building, running and testing at once, with no port collision and no per-checkout setup step.

Every worktree of one project resolves the same secrets file. A clone has nothing to configure. A colleague who pulls your branch gets their own port block on their own machine, because none of that state is in git — which is precisely why it is not: their block may already be taken there by one of their other worktrees, or by another project entirely.

What it means for git

Nothing to gitignore. Nothing git add -A can reach. Nothing npm pack can carry. Nothing an rm -rf on a clone destroys.

Delete the whole checkout and your secrets are still there.

That is the argument, and it is worth contrasting with the file it replaced. .dev.vars has to sit in a Worker’s directory because that is where wrangler reads it — so it is inside the checkout, and every project has to remember to ignore it. Nothing but the CLI reads the dev secrets file, so it can live somewhere better.

The dev secrets file is the source

Not a file something copies out of.

Edit a value there and the next pithy dev hands the Worker the new one, with no intermediate command. Delete one and it is gone from every generated file, with no stale copy anywhere to fall back to.

pithy secrets edit opens it, validates what comes back, and writes it atomically. It prints a path and a count, never a name and never a value.

How each Worker’s .dev.vars is generated

wrangler loads a .dev.vars from the directory it runs in and merges nothing, so each Worker needs its own — and pithy dev writes each one, from sources that never leave your machine:

  1. Every Secrets Store secret your registry declares, read from <project>/secrets.jsonc
  2. Plus whatever in <project>/dev.json no registry declares
  3. Overridden by the repo’s root .dev.vars.local
  4. Overridden in turn by that Worker’s own

Idempotent by content rather than mtime, so a second run writes no bytes and wrangler’s watcher has nothing to react to.

A .dev.vars Pithy did not write is never overwritten and never merged. It is named, .dev.vars.local is offered as the place for local values, and that Worker starts without one rather than with somebody’s file replaced underneath it.

Generation happens in pithy dev rather than in an install hook, because pithy dev is the command that runs every time — and an install hook runs before the values exist.

The ports registry, in practice

Keyed by main-checkout root, then by branch:

{
  "/home/jo/code/acme": {
    "main":            { "block": 0, "base": 8787, "size": 20 },
    "feature/12-auth": { "block": 1, "base": 8807, "size": 20 }
  },
  "/home/jo/code/other-app": {
    "main":            { "block": 2, "base": 8827, "size": 20 }
  }
}

The key is the checkout rather than the project name, because two unrelated projects can share a name and sharing a name must never mean sharing ports. Every worktree of one repository files under one key.

A checkout that is gone frees its ports. At the repo root the registry died with the checkout, so cleanup happened for free. Outside it nothing would, so every allocation prunes any root no longer on disk — and only a definite not there counts.

It cannot tell a deleted checkout from a moved one, and does not try. A moved repository’s blocks are freed and taken back the next time a Pithy command runs there. pithy doctor marks a row as not on disk, and that is the one line in its report you can act on: you renamed that directory.

It heals

The directory sits outside every checkout, so no clone and no git clean can take it. A wiped config directory, a new machine, or a relocated path still can — while the worktrees allocated from it live on.

So before allocating, a feature reclaims any block still pinned in an existing worktree’s own .dev.config.json. A lost registry cannot hand out a block a live feature is using.

That scan walks the worktrees of the repository the command was run in, and does not go looking through the others. So after a wipe each project re-registers its own the next time it runs, and a project that has not run since can be handed one of its blocks by one that has — which surfaces as a reported port conflict rather than two Workers on one port, because every port is verified on both loopback families before binding.

CI has none of this

No config directory, no file. Credentials arrive as environment variables, which is why <project>/tokens.json exists at all: pithy token mint writes a value there, you open it and copy the value into your CI provider’s secrets, and no Pithy command ever reads it back.

It is a handoff to a person, because CI cannot read either secret store and a credential has to cross that gap somehow.

Isolating it completely

PITHY_CONFIG_DIR says read config from here. It does not mean ignore the environment — CI legitimately means the first without the second.

PITHY_OFFLINE=1 is the second sentence: no ambient credentials, no network call. Set both and nothing in the CLI has a credential to authenticate with, which is the isolation people already believed the first variable gave them on its own.

ESC