---
title: "Review a release locally"
description: "Prepare a deterministic desired release on your own machine, generate the exact App Store Connect plan, and read it in an immutable local review — with no Apple mutation anywhere."
canonical: "https://shotops.dev/docs/mcp/workflows/local-release-review"
last_updated: "2026-09-07"
shotops_mcp_version: "0.9.6"
result_contract_version: "2.0.0"
---
# Review a release locally

Prepare a deterministic desired release on your own machine, generate the exact App Store Connect plan, and read it in an immutable local review — with no Apple mutation anywhere.

The local release commands answer one question: if this release were applied to App Store Connect, exactly what would change? They assemble the desired state from the `Tidebook 2.4 launch` project, read Apple, diff the two, and open the result in a read-only page on your own machine. Nothing at Apple is touched.

> Show me exactly what the Tidebook 2.4 release would change in App Store Connect without applying any of it.

Expected result: an immutable, short-lived review artifact opens on your computer and lists the exact proposed operations. App Store Connect remains unchanged.

## What this does, and what it does not

The 0.8 Local package ships five commands, and they are the whole release-review surface:

| Command | What it does |
| --- | --- |
| release setup | Validates local App Store Connect credentials and the tools already installed. |
| release prepare | Seals a deterministic desired-release workspace from a saved project. |
| release plan | Reads Apple and writes one immutable, short-lived exact diff. |
| release review | Serves that diff in a local read-only browser page. |
| release gc | Removes interrupted workspaces. Complete ones are never touched. |

> There is no apply step ShotOps cannot commit, upload, approve, submit for review or release. Not as a flag, not as a follow-up command, not as a next step you can talk it into. The plan is a document describing what would happen; applying it is `fastlane` or App Store Connect, run by you. A plan's operations carry names like `upload` and `attach` because that is what they describe — nothing executes them.

These are CLI subcommands of the local server, not MCP tools. No agent can invoke them and the hosted service has no equivalent.

## Set up owner-local credentials

```
npx shotops-mcp release setup

npx shotops-mcp release setup \
  --issuer-id <id> --key-id <id> --key-path ~/keys/AuthKey_ABC123.p8
```

Run with no flags it asks for the same three values interactively.

Setup validates rather than installs. The key must be an existing `.p8` file that you own and that only you can read — on macOS and Linux a key with any group or world permission bits is refused with the `chmod 600` fix in the message. It locates `fastlane` and Apple Transporter on your `PATH` without installing or updating either, then makes one authenticated read against Apple and refuses to save anything unless that read returns at least one team with at least one accessible app.

What it saves is the key's path, not the key. The file stays exactly where you put it. The command prints the three CI secret input names — `APP_STORE_CONNECT_ISSUER_ID`, `APP_STORE_CONNECT_KEY_ID`, `APP_STORE_CONNECT_PRIVATE_KEY` — and none of their values.

## Prepare the desired release

```
npx shotops-mcp release prepare --project "Tidebook 2.4 launch"
```

This assembles what the release should be: your repository's Fastlane metadata and app previews, plus panels rendered locally from the saved project, into a workspace directory — by default under `~/.shotops/release-workspaces/`, or wherever `--output` says.

It needs an owner sign-in (`npx shotops-mcp login`, or `SHOTOPS_TOKEN` for a non-interactive run), because it reads your project. Before rendering, it reads the target App Store Connect version's locales using your local release setup. Each selected locale not yet live on Apple is an advisory in the log and the workspace's readiness findings. You can continue preparing a new locale; `release plan` still requires its `fastlane/metadata/<locale>/name.txt` to create it. If Apple access is unavailable, preparation reports the skipped check and continues.

Preparation uploads nothing and changes no App Store Connect state. It reads your `Fastfile` and `Deliverfile` without executing them.

Preparing seals the release. Every byte a commit would need — the exact project record, the rendered screenshots, your app previews, the review attachment, compliance files and the signed build if you passed one — is copied into the workspace's own store, addressed by its SHA-256, in a directory only you can read. The workspace appears in a single atomic step: you either have a complete one or none at all. From then on the repository is no longer an input. Delete it, rewrite your `fastlane/` metadata, rebuild the `.ipa` — the plan you review and approve does not move.

Nothing about that leaves your machine. The sealed bytes are owner-local, and ShotOps holds none of them.

Useful flags: `--repo` for a repository root that is not the working directory, `--build` for a signed `.ipa`, and `--locales` / `--outputs` to narrow the scope. A locale or output you leave out is recorded in the workspace as partial scope rather than quietly ignored, so the plan that follows cannot present a subset as if it were the whole release.

## Create the exact plan

```
npx shotops-mcp release plan --workspace ~/.shotops/release-workspaces/<hash>
```

The workspace is the entire input — `plan` never reads your repository. Before it contacts Apple it reopens the workspace through its verifier and rehashes every sealed byte, and it does so again immediately before writing anything. A missing or altered blob stops the command by name, with no App Store Connect request made and no plan written.

The command reads App Store Connect directly with the credentials `setup` validated, diffs the live state against the workspace, and writes three files:

- `release-plan.json` — the full plan. Owner-local, and it stays that way.
- `release-review.zip` — a portable artifact carrying the exact plan and the screenshot bytes, which is what `release review` opens.
- `release-plan-envelope.json` — a separately redacted summary.

It prints the plan id, the target version and its App Store state, how many operations write and how many leave App Store Connect unchanged, when the approval window closes, and where each file landed.

Two clocks, not one. The plan stays readable for as long as its sealed workspace exists — you can open last week's plan and audit exactly what it would have done. Its approval window is one hour: after that the review still opens, and approving or committing needs a fresh plan, because an old diff is a wrong diff against a moving system.

Read the operation counts before the detail. A release you expect to be captions-only should be `set` and `replace` rows and nothing else; a `create` you did not expect means the plan is targeting a version that does not exist yet.

A plan is only invalidated by a change to something it actually touches. If a colleague edits a locale this plan does not write, the review reports that drift and stays valid; a change to a resource it does write, or to the app, version or its App Store state, blocks before anything is written.

App Store Connect is read and only read. No version is created, no bytes are uploaded, nothing is submitted, and no Apple credential reaches ShotOps.

## Review it in the browser

```
npx shotops-mcp release review
npx shotops-mcp release review release-review.zip
```

With no path it reads `release-review.zip` (or `release-plan.json`) in the current directory.

Before it serves a single byte, the command verifies the exact plan hash, the artifact manifest and every screenshot in it. Only then does it bind a random port on `127.0.0.1` and open a URL containing a random nonce. The page loads no remote resources, runs no scripts, and offers no edit, approve or send action — the server answers `GET` and `HEAD` and returns `405` to every other method. A plan past its approval window still opens, marked read-only and saying so at the top; a corrupt, edited or content-mismatched input fails before the server starts.

Nothing about that page can change anything. It is a document you read, disagree with, and then act on somewhere else.

## Review from another device

```
npx shotops-mcp release review --remote
```

Everything above is local and stays that way. The flag is the one exception, and you have to type it: it uploads the plan's readable review — the app name, the version, the listing diff and the rendered screenshots — to ShotOps and prints a `shotops.dev/release/<plan>` link. Open that link on a phone or a second computer, signed in as the account that owns the project, and you get the same facts the local page shows, plus an Approve button.

The hosted page proves what it read. Being signed in says who is clicking, and the plan hash is already in the link, so the page instead sends the SHA-256 of the review content it actually rendered. ShotOps compares it with what the plan carries and refuses anything else, so a tab left open across a second `release review --remote` cannot approve content nobody looked at.

The guarantee for this path is its own, and narrower than the local one:

- The uploaded review is held only until you approve or the one-hour approval window closes, whichever comes first. It is then deleted, and it is never logged.
- Your Apple credential is not involved, on this path or any other. Neither is the full plan — only the readable review is uploaded.
- The write to App Store Connect still happens on the machine that holds your key. Approving on your phone records a decision; `release commit` runs where it always did.

Without the flag nothing is uploaded and there is nothing to open: a plan reviewed locally has no hosted page, and the link for it does not exist.

## Clean up interrupted workspaces

```
npx shotops-mcp release gc --dry-run
npx shotops-mcp release gc
```

A prepare that was interrupted mid-seal leaves a discoverable temporary directory rather than a half-built release. `gc` removes those once they are more than 24 hours old, and only those: a complete workspace is a record you own, so it is never deleted for you. Symbolic links are skipped rather than followed. Use `--older-than <hours>` to change the window and `--root` to point at a workspace directory other than the default.

## Where zero-custody stops

ShotOps is zero-custody: no surface of it ever holds an Apple or store credential. This workflow is where that boundary is most visible, so here it is seam by seam.

| Seam | Where it happens | What ShotOps holds |
| --- | --- | --- |
| The .p8 private key | Stays at the path you gave setup | The path. Never the key, never a copy. |
| Authenticating to Apple | Your machine, your key | Nothing. No Apple credential is sent to ShotOps. |
| Reading App Store Connect | Your machine, during release plan | Nothing. The snapshot stays local. |
| The sealed release bytes | Your disk, 0700 | Nothing. Screenshots, previews, attachments and build bytes never leave the machine. |
| The full plan and screenshots | Your disk | Nothing. Only the redacted envelope is even shaped to be shareable, and this workflow does not send it. |
| The review page | 127.0.0.1, nonce URL, read-only | Nothing. No plan content, screenshot or credential leaves the machine. |
| Applying the release | Your fastlane, your App Store Connect | Nothing. ShotOps has no write path to Apple at all. |

This is also why the workflow is local-only. A hosted service that could prepare a release for you would have to hold your Apple credential, and that is the one thing ShotOps is built not to do. The same rule governs the web app: there is no store-link, drift or push UI there either.
