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. The CLI and the MCP connection are the supported interfaces to Pharen; the HTTP API behind them is not yet a public contract, so its paths and fields can change.
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 organization.
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 | How long to wait for that first event, typed with its unit: 30s, 2m. Default: 3m. |
--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-url | optional | Where this command talks — the control plane, as on every other command. |
--api-base-url | optional | Where your app will send its events: a development override written into the generated xcconfig as PHAREN_API_BASE_URL (escaped for you). Normally absent — the SDK compiles in the production host. |
--org | optional | Assert your organization. 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 active
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 organization
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 organization is authoritative, and a token/config mismatch is a hard 403.
Every command that talks to the control plane needs a credential (init ios is purely local unless you pass --mint). On your machine, pharen auth login signs you in from a browser and stores the session. A CI job can sign in as itself, with no secret to store — see pharen workload. Elsewhere, set PHAREN_AUTH_TOKEN as a CI secret and never commit it. 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 → live), with a development-key fallback for any configuration it doesn't name — never the live 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 organization 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-url | optional | Where this command talks — the control plane, as on every other command. |
--api-base-url | optional | Where your app will send its events: a development override written into the generated xcconfig as PHAREN_API_BASE_URL (escaped for you). Normally absent. |
--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.
- Environment alignment — Release carries a
phi_live_…key, the othersphi_test_…, and the unnamed-configuration fallback is never live. - 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 active 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 environment --wait watches. Default: development. |
--timeout | optional | How long to wait for the first event, typed with its unit: 30s, 2m. Default: 3m. |
--dir | optional | App directory to inspect (auto-detected otherwise). |
--org / --app / --api-url | optional | Override the resolved config. |
pharen releases create
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 create --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 to the release. |
--commit-range | optional | Commit range for this release. |
--release-track | optional | testing | released. testing declares a build handed to testers; released declares it in users' hands, which stamps when it was declared released and moves its rows onto the released track. Without the flag the release is registered without declaring a track; a released release never moves back to testing. |
--org / --app / --api-url | optional | Override the resolved config. |
pharen builds upload
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 builds upload --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). |
--no-latest | optional | Leave the stable "latest" pointer where it is. |
--expires-in | optional | How long the public install link stays live, typed with its unit: 7d, 12h. |
--org / --app / --api-url | optional | Override the resolved config. |
pharen symbols upload
Uploads debug symbols, kept private under your organization. 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 symbols upload ./App.dSYM
pharen symbols upload ./App.xcarchive/dSYMs # every bundle in the folder: your app's, and any a framework brought| Flag | Required | Description |
|---|---|---|
<paths> | required | One or more .dSYM bundles (or the DWARF binaries inside them), or a directory to search: an archive's dSYMs/ folder, an .xcframework, the SwiftPM artifacts directory. Every bundle found uploads on its own; a directory holding none is refused; symlinks inside a searched directory are not followed, and the path you pass is taken as given. |
--platform | optional | Override the inferred platform. |
--uuid | optional | Declare debug ids explicitly (repeatable); otherwise read from the binary. The ids belong to one binary: pass that DWARF binary (inside the bundle's Contents/Resources/DWARF/) when overriding — a bundle can hold two. |
--type | optional | auto | dsym. |
--org / --app / --api-url | optional | Override the resolved config. app is informational only — symbols join by debug id. |
pharen ingest-keys create
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 an environment later, or to replace a key.
pharen ingest-keys create --environment live| Flag | Required | Description |
|---|---|---|
--environment | required | live | test | staging | development. |
--platform | optional | Scope the key to a platform. |
--org / --app / --api-url | optional | Override the resolved config. |
pharen workload
Lets a CI job sign in as itself, with nothing to store or rotate. An organization owner binds one pipeline to a scope; each command in that job then signs in on its own, with a credential that lasts at most fifteen minutes and can write only builds, symbols and releases.
# GitHub: the numeric ids (gh api repos/OWNER/REPO --jq '.id, .owner.id')
pharen workload federations add --kind github --project 123456 --project-owner 9 --environment test
# GitLab.com: the exact group/project path
pharen workload federations add --kind gitlab --project acme/app --environment test
pharen workload federations list
pharen workload token # in the job: its organization, scope and expiry, never the tokenA GitHub job needs permissions: id-token: write, and can run any number of pharen commands. A GitLab job needs id_tokens: { PHAREN_ID_TOKEN: { aud: 'https://api.pharen.ai' } } and must run on a protected branch; its token signs in once, so each pharen command needs its own token (workload token counts as one): declare one id_tokens: entry per command and pass each in turn, as in PHAREN_ID_TOKEN="$PHAREN_ID_TOKEN_2" pharen …. The release lane takes five, PHAREN_ID_TOKEN_1 through PHAREN_ID_TOKEN_5, and hands each command the next.
| Flag | Required | Description |
|---|---|---|
--kind | required | github | gitlab. |
--project | required | GitHub: the numeric repository id. GitLab: the exact group/project path. |
--project-owner | GitHub required | The numeric owner id. |
--environment | optional | An environment the job may act in; repeat for more. Omitted means every environment, live included, which then also needs --job-environment or --ref. |
--app | optional | An app the job may act in; repeat for more. |
--job-environment | optional | GitHub: the deployment environment the job must run in. Required for a scope that reaches live. |
--ref | optional | GitLab: the exact protected ref. Required for a scope that reaches live. |
--label | optional | A name to recognize the binding by. |
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 | list is which apps your organization 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. edit sets an app's name and tagline; 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 environments | list is your organization's environments, the names --environment accepts: live always, and every test environment you registered. create registers a test environment, edit changes its label, and archive retires one for good — its name is never used again, and live is never archived. See Environments & build channels. |
pharen ingest-keys | Beyond create: list, edit, rotate, revoke and restore — 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 device-requests | The tester provisioning queue: list 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; set shake.enabled false is the remote switch for shake-to-send-feedback, addressed by the same path the served document spells it with — it can turn the feature off in the field, and can never turn on an app that compiled it off; unset drops the override.The same set and unset write the stored feedback.nps, feedback.csat and feedback.copy knobs and each prompt kind's launch_limit — for example config set feedback.csat.launch_limit exempt or config set feedback.nps.cooldown_days 90d, a duration typed with its unit. A text knob writes the default language only and keeps every translation already stored; translations are written in the console. config subcategories list|show|create|edit|remove reads or writes one entry of a report category's stored subcategory list, and config checkpoints list|show|create|edit|remove one stored event checkpoint — a rule over the app's own event and how many times it has been counted — for the rating, nps or csat prompt. Each write reads the stored configuration, changes the one knob or entry, and writes the rest back as it was read. A write that lands between that read and this command's write is replaced by it; the command then names the replaced revision — or, when the history cannot show it, the versions to review — and exits non-zero. The platform's refusals and advisories are printed as it words them, including the advisory that a build applies these settings only from an SDK release that reads them, and the one for a rule naming an event the platform has not seen the app send.config notices list|show|create|edit|remove writes one entry of the app's own app_notices — an update gate, a retirement or an outage screen; a banner or sheet is stored but not drawn by the current SDKs. --platform android targets the Android SDK, which is in private beta, and --region is judged by that SDK alone: the iOS SDK doesn't show a notice that names a region. A whole-app kill switch is not among them: this CLI refuses to create or edit one, because the server only accepts a kill write from an owner's interactive console session, which this CLI can never carry. config brand show|edit sets the colors, corner radius and font of the screens and forms the SDK draws, the same flags as ingest-keys edit --publishable --accent …. |
pharen push | push credentials is the APNs and FCM key material, read from disk and never pasted: add, bind to an app, rotate, disable, and test sends one test push to one device. push registrations list is 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. list --status 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. |