Wizard Prefill#

What Is It?#

Wizard prefill is a way to hand psw wizard a JSON file that already contains everything the first wizard page would ask you — your domain, your SSH key path, your OPNsense and Cloudflare credentials, and optionally your SMTP relay settings — so PSW can skip that page entirely and drop you straight on the next step (Hardware).

You run it like this:

psw wizard --project ~/homelab1 --prefill ~/.config/psw/homelab1.json

PSW reads the file, runs the exact same setup pipeline the first page’s “Initialize Project” button would run, and then opens the wizard in your browser already one step ahead.

Why Does It Exist?#

Typing (or copy-pasting) the same domain, SSH key, two API tokens, and SMTP credentials every time you rebuild a test project from scratch gets old fast. One JSON file you keep around is a lot less friction than a page full of inputs to re-fill each time.

It’s also useful if you’re writing a script or CI job that needs to spin up a PSW project reliably — a JSON file is easier to version and diff than a sequence of form clicks.

When Should You Use It?#

  • Always an option — the page still exists, the flag is optional. If you skip --prefill, the wizard behaves exactly like before.
  • Best for fresh projects. PSW will refuse to use --prefill on a project directory that already has a project.yml — that directory has already been through Start, and re-applying prefill would risk overwriting your existing credentials. If you want to resume an existing project, drop the flag.
  • NEVER commit the JSON to git. It contains your OPNsense API key and secret, your Cloudflare API token, and (if you configured one) your SMTP password. Two safe options:
    • Keep it outside your repo entirely (e.g. ~/.config/psw/<name>.json).
    • Keep it inside the repo but name it *.prefill.json — the PSW monorepo’s root .gitignore has a blanket rule that ignores any file matching that glob, anywhere in the tree. That way the file lives next to the code it feeds without any risk of being committed.

What’s In The File?#

A minimal prefill looks like this:

{
  "domain": "example.com",
  "ssh_key": "~/.ssh/id_ed25519",
  "acme_email": "you@example.com",

  "opnsense_url": "https://10.10.0.1",
  "opnsense_api_key": "KEY",
  "opnsense_api_secret": "SECRET",

  "cloudflare_token": "CFTOKEN"
}

That’s enough to run the Start pipeline. Every field there maps 1:1 to a field on the first wizard page.

Convenience: point to a file instead of inlining secrets#

If you’ve already got an OPNsense key.txt download (System → Access → Users → edit root → API keys → +) and/or a Cloudflare token saved in a file, you can point the prefill at those files instead of pasting their contents:

{
  "domain": "example.com",
  "ssh_key": "~/.ssh/id_ed25519",
  "acme_email": "you@example.com",

  "opnsense_url": "https://10.10.0.1",
  "opnsense_key_file": "./opnsense-keys.txt",

  "cloudflare_token_file": "./cloudflare-token.txt"
}

The paths are relative to the JSON file’s directory, not to whatever directory you ran psw wizard from. That way the JSON and its referenced files travel as one folder you can move around freely. Absolute paths (like /home/you/secrets/cf.txt) also work.

You can mix and match: file pointer for OPNsense, inline token for Cloudflare. But for one provider you can’t do both at once — PSW will refuse with a clear error if you try.

Optional: SMTP relay#

If you want Proxmox system notifications, Authelia password resets, Grafana/Alertmanager alerts, etc. to go out by email, add an smtp section:

{
  "smtp": {
    "host": "smtp.gmail.com",
    "port": 587,
    "username": "you@gmail.com",
    "password": "<gmail-app-password>",
    "from": "you+psw@gmail.com",
    "tls": true
  }
}

Omit the whole smtp block if you don’t want email notifications — PSW will write nothing to secrets/smtp.yml and you can wire it up later with psw node configure-smtp.

See docs/examples/wizard-prefill.example.json for a complete starting point.

Recovery mode: carry settings forward into a new project#

Which recovery scenario does this serve? Recovery-mode prefill is the headless equivalent of the wizard’s “Restore from a previous project” card — and like the card, it covers Scenario B from the recovery story: “carry settings forward into a brand-new project”. You get a fresh project shape (the wizard runs Start the way it would for any new project) but your Cloudflare token, OPNsense credentials, and SMTP relay come from the panic-backup tarball.

If you’re trying to do Scenario A (“bring my old project back” — git remote already has the project shape, you just need the age key), that’s a separate flow that doesn’t go through prefill today; the manual 3-line recipe is in secrets.md § Scenario A , and a wizard UI for it is planned at docs/plans/wizard-key-recovery/ .

Not sure which scenario you’re in? Read disaster-recovery.md — it’s the umbrella with the “do I have my git remote?” decision tree and the three-layer story (project shape + secrets + per-app data).

If a previous PSW project of yours blew up, you’ve already saved a recovery tarball off-box (the panic-backup file made with psw project export-recovery — see secrets.md § Reusing Secrets Across Project Rebuilds ). The prefill can carry that file too. PSW unpacks the panic backup, takes the old project’s OPNsense / Cloudflare / SMTP credentials straight from inside it, and lands you on the Hardware step exactly the way the regular prefill does — except now you keep all your old identities. Same passwords, same tokens.

The headless way to do what the wizard’s “Restore from a previous project” card does in a browser:

{
  "domain": "casaeureka.ca",
  "ssh_key": "~/.ssh/id_ed25519.pub",
  "acme_email": "admin@casaeureka.ca",

  "restore_from": "~/psw-recovery/casaeureka-2026-04-30T14-00-00Z.tar.gz"
}

That’s it. One extra field. The bundle has the encryption key and the encrypted secrets, so PSW can do everything from there.

When you’d add age_key too#

Only one situation: you deliberately kept your age key in a different place from the bundle. Some operators do this on purpose — bundle on a USB stick in the office, age key in a safety-deposit box across town — so a thief who grabs the USB still can’t decrypt your secrets. If that’s you, point age_key at the matching key file:

{
  "...": "...",
  "restore_from": "~/usb/casaeureka-2026-04-30T14-00-00Z.tar.gz",
  "age_key": "~/safety-box/casaeureka.age-key"
}

PSW makes sure the key actually matches the bundle’s encrypted files before installing anything — if you grabbed the wrong key by mistake, you’ll see the error at the picker, not deep inside bootstrap. The age_key file must be mode 0600, same rule as the prefill JSON itself.

If your bundle and key live together in the same backup spot (the common case), just leave age_key out — PSW finds the key inside the tarball and uses it.

Why other fields disappear in recovery mode#

Notice the example above doesn’t have cloudflare_token, opnsense_url, or any of the other credential fields you’d see in a regular prefill. They come from the recovered tarball — including them again would be re-typing what the panic backup already remembers.

You can still include them if you want to change one at restore time (e.g. you’re rotating your Cloudflare token and don’t feel like re-exporting the bundle just for that). PSW treats any credential field present in the JSON as an override, applied on top of whatever the bundle had.

domain, ssh_key, and acme_email always come from the JSON. Those live in project.yml, which is not part of the panic backup — project.yml is in your off-site git remote, where it belongs.

Oops-protection#

  • Pointing at an age_key file but forgetting restore_from → refused. An encryption key with nothing to unlock is a typo, not an intent.
  • Pointing at a restore_from file but forgetting --project on the psw wizard command → refused. PSW needs to know which folder to install the recovered files into.

Permissions#

Because the file contains plaintext secrets, PSW refuses to read it unless it’s mode 0600 (readable only by you). Same reflex as OpenSSH with your private key. Fix it with:

chmod 600 ~/.config/psw/homelab1.json ~/.config/psw/*-keys.txt

The referenced files (like the OPNsense key.txt you pointed at) get the same check.

What Happens When You Run It#

  1. PSW validates the JSON — wrong shape, unknown field, loose permissions → clear error in your terminal and non-zero exit.
  2. PSW probes OPNsense to make sure the URL and credentials work.
  3. PSW reads your DHCP subnet + pool range from OPNsense Kea .
  4. PSW writes project.yml, network.yml, and encrypts your provider secrets into secrets/providers.yml (and your SMTP settings into secrets/smtp.yml if you configured them).
  5. PSW runs project init , registers the local-DNS and DHCP providers, stamps the network-verified checkpoint , initializes git, and commits the scaffold.
  6. PSW opens the wizard in your browser — already on the Hardware step, with Start marked complete.

If any step fails, PSW prints the failing step in your terminal with the reason, and exits non-zero. No partial project is left behind — each failure point is before the next step’s disk write, so a failed prefill run is safe to fix-and-retry against the same directory (once you’ve removed the scaffold the failing step may have already written; for a truly fresh retry, rm -rf the project dir and run again).

What It Does NOT Do#

  • Does not automate later pages. Only the Start page is covered. You still click through Hardware → Plan → Install → Launch manually in the browser.
  • Does not auto-click “Test OPNsense” / “Test Cloudflare”. The prefill runs the probes the final submit runs anyway, so the buttons would be redundant.
  • Does not support encrypted JSON. The file is plaintext. If you want to keep it encrypted at rest, decrypt it into a tmpfs directory before running and remove the decrypted copy after.

Safety Notes#

  • The file lives outside your PSW project. Never commit it; your project’s .gitignore covers your project — not your home directory.
  • PSW binds the wizard to 127.0.0.1 by default, so the form’s inputs (and the rendered HTML) are only visible to your local machine. If you ever change --host to a LAN interface, don’t use prefill — the secrets would render into a web page on a network you don’t fully control.
  • --prefill does not change where secrets end up at rest. They go into the same SOPS-encrypted files (secrets/providers.yml, secrets/smtp.yml) the web form uses, decrypted only by the age key PSW generated when it ran project init.

See Also#