Reference
CLI reference
The pharen command-line tool is the thin, authenticated client behind distribution and symbol upload. The fastlane plugin and the Xcode run script wrap it — this page is the underlying surface.
pharen setup ios
The whole on-ramp as one command: sign in, write the project files, mint your ingest keys, wire the project, verify. What's left afterwards is one line of your own Swift.
pharen setup ios # sign in, configure, wire, verify
# ── call Pharen.start(), build and run the app ──
pharen verify --wait # confirm your first event arrivedsetup doesn't watch for your first event, because on a first run nothing can have arrived — you still have to build. The confirmation is its own command, whose name is what it does, and setup prints it at the moment it becomes useful, so there is nothing to remember. (setup --wait exists as sugar when you already have the app running and want one command end to end.)
In the ordinary case it takes no arguments at all. Your org is derived from your credential, and your app id is proposed from the project's own bundle identifier (case-folded, so com.Acme.MyApp and com.acme.myapp are the same app) and confirmed with you before it is used. So there is nothing to look up and nothing to get wrong. --app overrides the proposal; if you pass --org and it disagrees with your credential, that's a hard stop rather than a silent switch to the wrong tenant.
It is stateless and idempotent: every step declines to redo work that is already done, so re-running always resumes and never repeats. That is why “run it again when you've wired it up” is safe, and why a re-run can't issue a second set of keys.
Wiring your project
Where your project is built from a project.yml (XcodeGen), setupalso points it at the generated config, adds the SDK package, and declares PharenIngestKey in your Info.plist. It prints every edit — file by file, line by line — and asks before writing any of them. You know whether your working tree is clean and whether now is the moment; the tool doesn't.
-y applies without asking (for CI and scripted runs) and --no-wire skips the step. With no terminal and no -y the answer is no, the plan is still printed, and the run continues — it never blocks waiting on input that can't arrive. Declining is a correct answer, not a failure: your keys are minted either way and verify reports what is still unwired.
Edits are additive only. Where anything is ambiguous — an existing configFiles: block pointing elsewhere, an inline include: [...], a hand-managed .xcodeproj — it declines and tells you what to do by hand rather than guessing at your project file. The SDK is linked to your app target only, never to App Clips or extensions.
| Flag | Required | Description |
|---|---|---|
--app | required | The app id. The only input; org comes from your credential. |
-y, --yes | optional | Apply the project edits without asking. For CI and scripted runs. |
--no-wire | optional | Never touch project.yml or Info.plist; just report what is unwired. |
--wait | optional | Also watch for the first event once configuration finishes. Off by default. |
--timeout | optional | Seconds to wait for that first event. Default: 180. |
--no-login | optional | Never start the browser sign-in; fail fast if there is no credential. For CI. |
--environment | optional | Repeatable; mint only these environments instead of all three. |
--api-base-url | optional | Development override written into the generated xcconfig (escaped for you). |
--org | optional | Assert your tenant. Derived when omitted; a mismatch is a hard stop. |
--team-id / --dir / --force / --no-lane | optional | Passed through to init ios. |
What the CLI touches on your machine
setup and verify read your source and edit your project files, so here is the whole of it — the answer you would otherwise have to get by reading the code.
What it reads
To diagnose an integration, verify parses your project.yml, every Info.plist under your app directory, and every .swift file there (looking for a Pharen.start call). None of it is transmitted. Those reads feed a regular expression and a substring test inside the process and are then discarded. Across a whole run — --wait included — the CLI makes three requests, all GET, all to the API host you configured:
GET /v1/auth/session does this credential still authenticate
GET /v1/ingest-keys?app_id=… are the keys in your build still live
GET /v1/events?app_id=&environment=… has anything arrived yet (--wait only)No request body, no file contents, no file paths, no source. That isn't a promise in a document: the test suite runs the whole command over a tree whose files carry distinctive markers and fails if any marker turns up in an outgoing URL, header or body. The assertion exists to break if anyone ever adds telemetry to a check.
The directory walk is bounded to your app directory and never follows a symlink — not into a directory, not to a file whose name happens to match. It also stops at a depth and entry budget, and reports when it did, so a partial answer never reads as a clean bill of health.
What it writes
.pharen.yml,release/andConfig/Pharen.xcconfig— files Pharen owns. It refuses to overwrite an existing one unless you pass--force.project.ymland yourInfo.plist— files you own. Only additions, only after it prints every line and you say yes, and never where the situation is ambiguous. This is the step-yand--no-wirecontrol.~/.config/pharen/credentials.json— your session frompharen auth login, written0600in a0700directory. Nothing else ever writes a credential to disk, andpharen auth logoutdeletes it.
Configuration
Three inputs configure every command. In precedence order, highest first:
- Command-line flags —
--org,--app,--api-url. - Environment —
PHAREN_AUTH_TOKEN(the one secret) andPHAREN_API_URL(development only; see below). .pharen.yml— your committed org and app.
org: your-org # customer-facing alias of your tenant
app: your-app-id # the app_id.pharen.yml is found by walking up from the working directory — the nearest one wins — so it works whether a command runs from the repo root or an app subdirectory. It holds no secrets. Your --org never grants access on its own: the auth token's tenant is authoritative, and a token/config mismatch is a hard 403.
PHAREN_AUTH_TOKEN is a secret — set it in your CI environment, never commit it. Every command that talks to the control plane needs it (init ios is the exception — it's purely local, unless you pass --mint). Interactively, pharen auth login is the friendlier path: it signs you in from any browser and stores the session, so no token has to be handled at all. Output is JSON on stdout, so Fastlane and CI can parse it.
pharen init ios
Sets up a repo: writes .pharen.yml at the root and copies the release lane into your app. Local only — no network, no prompts. Re-running won't overwrite existing files unless you pass --force.
pharen init ios --org your-org --app your-app-idAdd --mint and it also issues your ingest keys and writes them into your build settings — the whole on-ramp in one command:
pharen init ios --org your-org --app your-app-id --mintThis mints one key per environment and generates a committed Config/Pharen.xcconfig that assigns them per build configuration (Debug → development, Staging → staging, Release → production), with a development-key fallback for any configuration it doesn't name — never the production key. It ends with #include? "Pharen.local.xcconfig", so a gitignored per-machine override still wins if you keep one. Everything else about init ios stays local.
--mint needs a credential. Either sign in first with pharen auth login, or add --login to run that flow as part of this command. It's opt-in rather than automatic because the sign-in blocks waiting for you to approve in a browser, and this command also runs in lanes — without the flag, a missing credential fails in a second and names what to run.
| Flag | Required | Description |
|---|---|---|
--org | required | Your tenant alias. |
--app | required | The app id. |
--mint | optional | Also issue ingest keys and write Config/Pharen.xcconfig. Needs a credential. |
--login | optional | With --mint: run the browser sign-in inline instead of failing. Opt-in — it blocks on a human, and lanes run this command. |
--environment | optional | Repeatable; mint only these environments instead of all three. |
--api-base-url | optional | Development override written into the generated xcconfig (escaped for you). |
--team-id | optional | Apple team id for exportOptions.plist (derived from Xcode when omitted). |
--dir | optional | App directory to write release/ into (auto-detected otherwise). |
--force | optional | Overwrite existing .pharen.yml / release files, and regenerate the xcconfig. |
--no-lane | optional | Write only .pharen.yml, skip the release lane. |
pharen verify
Checks an integration end to end and tells you what is wrong. Every way an iOS integration can be misconfigured fails silently — the SDK is deliberately quiet when it isn't configured, so a broken setup looks exactly like a working one until you go looking for data that was never sent. This is where you find out instead.
pharen verify # check the setup
pharen verify --wait # check it, then watch for your first eventIt asserts, in order:
.pharen.ymlresolves an org and app.- Your control credential authenticates — not merely that a token is set.
- The generated build config exists, and each configuration has a well-formed key.
- Ring alignment — Release carries a
phi_live_…key, the othersphi_test_…, and the unnamed-configuration fallback is never production. - No value was truncated by the xcconfig
//comment rule. - The project actually reads the xcconfig, and some
Info.plistdeclaresPharenIngestKey. These two are best-effort file checks, so they warn and name what they looked at rather than failing your build. - The keys baked into your build are still live server-side, and none was revoked.
- With
--wait: your first event arrives — reported with the version and build it came from, so you know it was this build talking.
Each check reports pass, fail, warn, or skipped; a check that couldn't run says so rather than silently counting as a pass. Exit is non-zero on any failure, and the JSON on stdout carries the same list — so it works as a CI gate.
| Flag | Required | Description |
|---|---|---|
--wait | optional | Poll until the first event arrives from this app. |
--environment | optional | Which ring --wait watches. Default: development. |
--timeout | optional | Seconds to wait for the first event. Default: 180. |
--dir | optional | App directory to inspect (auto-detected otherwise). |
--org / --app / --api-url | optional | Override the resolved config. |
pharen releases new
Registers or updates a release — the anchor incoming crashes resolve against. Idempotent: re-running for the same version and build is a safe no-op.
pharen releases new --platform ios --version 1.2.0 --build 42 --commit "$(git rev-parse HEAD)"| Flag | Required | Description |
|---|---|---|
--platform | required | ios | android | web. |
--version | required | Marketing version (CFBundleShortVersionString on iOS). |
--build | required | Build number (CFBundleVersion on iOS). |
--commit | optional | Commit sha, attached as provenance. |
--commit-range | optional | Commit range for this release. |
--finalize | optional | Mark the release as deployed. |
--org / --app / --api-url | optional | Override the resolved config. |
pharen upload-build
Uploads the installable and returns the OTA install link. On iOS, --bundle-id is required — the server generates the install manifest and the device refuses a mismatch.
pharen upload-build --platform ios --version 1.2.0 --build 42 \
--bundle-id com.yourcompany.yourapp ./App.ipa| Flag | Required | Description |
|---|---|---|
<path> | required | Positional: path to the .ipa (or .apk). |
--platform | required | ios | android. |
--version / --build | required | Release identity. |
--bundle-id | iOS required | CFBundleIdentifier; used to generate the install manifest. |
--title | optional | Install-dialog title (defaults to the app id). |
--skip-latest | optional | Leave the stable "latest" pointer where it is. |
--expires | optional | Time-to-live for the build. |
--org / --app / --api-url | optional | Override the resolved config. |
pharen upload-symbols
Uploads debug symbols, kept private under your tenant. Slim by design — a token and a path is a complete invocation. Platform is inferred from the artifact (a dSYM implies iOS), and the server deduplicates by debug id, so re-uploads are cheap no-ops that still exit 0.
pharen upload-symbols ./App.dSYM| Flag | Required | Description |
|---|---|---|
<paths> | required | One or more .dSYM bundles (or the DWARF binaries inside them). |
--platform | optional | Override the inferred platform. |
--uuid | optional | Declare debug ids explicitly (repeatable); otherwise read from the binary. |
--type | optional | auto | dsym. |
--org / --app / --api-url | optional | Override the resolved config. app is provenance only — symbols join by debug id. |
pharen ingest-keys new
Mints a single ingest key for one environment — the phi_live_… / phi_test_… value behind PharenIngestKey. The secret is shown once, and this command hands it to you, so you place it yourself. For setting up an app, prefer pharen setup ios (or pharen init ios --mint when scripting), which mints every environment's key and writes them into your build settings for you. Reach for this one to add a ring later, or to replace a key.
pharen ingest-keys new --environment production| Flag | Required | Description |
|---|---|---|
--environment | required | production | staging | development. |
--platform | optional | Scope the key to a platform. |
--org / --app / --api-url | optional | Override the resolved config. |
Everything else
The commands above are the ones you meet while setting an app up and shipping it. The rest of the tool is listed here so you know it exists; each one documents itself in full with pharen help <command>.
| Command | What it does |
|---|---|
pharen apps | Which apps your tenant has, according to the platform — the list to check an app id against, since a typo otherwise produces a working command pointing at nothing. Archived apps are listed by default (an id is never reused); --live narrows it. pharen apps archive retires one. |
pharen auth | login signs you in from any browser and stores a session; status exits 0 when the active credential authenticates; logout deletes it. |
pharen ingest-keys | Beyond new: list, rotate and revoke — the lifecycle of the key your app ships with. See Keys & credentials. |
pharen query-keys | Mint, list, rotate and revoke the read-only key you hand to a bot or an agent. See Keys & credentials. |
pharen devices | The tester provisioning queue: requests prints portal-pasteable UDID/label lines for testers whose device isn't registered yet, and resolve closes one out. |
pharen config | The remote configuration your builds resolve. show prints the stored configuration and its version; shake is the remote switch for shake-to-send-feedback — it can turn the feature off in the field, and can never turn on an app that compiled it off. |
pharen push-credentials | APNs and FCM key material, read from disk and never pasted: add, bind to an app, rotate, disable, and test-send to one device. |
pharen push-registrations | One row per device registration, newest first. Structurally token-free — no push token is ever printed. |
pharen diagnostics | Field diagnostic bundles: what has arrived, what is still waiting, and how much room is left. pending prints, for each waiting bundle, the one sentence that unblocks it. |
pharen stores | Connect App Store Connect and read what Apple measures about your app from outside it — ratings and reviews, keyword and chart rank, downloads and proceeds. |