Skip to content
Back to skills

Verify And Land

ASecurity

Build the checked-out branch, launch that exact app, let the user confirm the fix is really there, then push and land it by the project's landing mode.

  • 2 stars
  • 0 votes
  • 0 copies
  • 5 views
  • Added September 5, 2026
developmentpythonrustgoswiftbashexpressgit

Works with

  • cli
  • mcp

Security analysis

A100/100

Scanned October 3, 2026

npx -y skills add Paretofilm/superpowers-gstack --skill verify-and-land --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Verify And Land?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Verify And Land
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/paretofilm-verify-and-land/badge)](https://www.skillsdirectory.com/skills/paretofilm-verify-and-land)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: verify-and-land
description: |
  Build the checked-out branch, launch that exact app, let the user confirm the
  fix is really there, then push and land it by the project's landing mode.
---

# Verify, then land

The gap this closes: a fix is committed, the user opens "the app", the fix is not
there — so they conclude it did not work. Usually it did. They were looking at a
**different build**: the copy in `/Applications` that Spotlight, the Dock and
Launchpad all open, or a `DerivedData` product from another branch. Nothing rebuilds
when the branch changes, because nothing ever does. Measured on one real project: the
installed copy was a month older than the branch build, and four other stale build
products were registered besides.

So this skill never asks the user to open the app. It **builds the branch they are
standing on, launches that exact bundle, proves on screen which one is running**, and
only then asks whether the fix is there. A yes lands the branch the way the project
has decided to land (Phase 7), without a second question unless the branch itself
changes how the project lands; a no keeps the branch open and says what to look at next.

Invoke with: `/superpowers-gstack:verify-and-land`

## Phase 0 — refuse early, and commit first

| Condition | Do this |
|---|---|
| Not a git repo | Refuse: "Not a git repository — there is no branch to verify or land." |
| No runnable app (Phase 2 finds none) | Refuse: "No app target found. This skill verifies something you can look at; for a library or CLI, run the tests instead." |
| Detached HEAD | Refuse landing later, so say now: give the work a branch first (`git switch -c <name>`), then re-run. |
| On the default branch | Build and launch normally, then skip Phase 7 — there is nothing to land. Say so once. |

**Uncommitted changes must be committed before the build.** Not a style preference:
the build compiles the working tree, but `git push` and every landing skill move
*commits*. Verify a dirty tree and land it and the user has approved something that
is not what shipped. Offer to commit — with a real message, per the repo's
convention — and only then build. If they decline, build anyway so they can look, but
**Phase 7 is blocked**: say plainly that landing would ship something other than what
they just saw.

## Phase 1 — name the change, so the user knows what to look at

```bash
DEFAULT_REF=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/||')
[ -z "$DEFAULT_REF" ] && for c in origin/main origin/master main master; do
  git rev-parse -q --verify "$c" >/dev/null && DEFAULT_REF=$c && break
done
BASE=$(git merge-base HEAD "$DEFAULT_REF")
git log --oneline "$BASE..HEAD"; git diff --stat "$BASE..HEAD"
```

Keep the *ref* that verified, not a stripped name — in a fresh clone `origin/main`
exists and `main` may not, and `merge-base` against a missing ref fails.

Turn the diff into **one sentence per observable change**, not files: "the sidebar
should keep its width when you resize the window" beats "3 commits touching
`SidebarView.swift`". A branch often carries more than one change — name each,
because the gate in Phase 6 asks about all of them: one yes must never quietly ship
three changes of which the user looked at one. If a change has no user-visible
effect — a refactor, a test — say so: looking is the wrong check for it; the tests
cover it and the gate covers the rest.

## Phase 2 — resolve the container, the scheme, and the destination

**Generate the project first if it is generated.** A `project.yml` is itself an
Apple-project signal: many repos commit only the spec and gitignore the
`.xcodeproj`, so listing schemes before generating finds nothing at all. If
`project.yml` exists and either no `.xcodeproj` is present or the spec (or anything
it includes) is newer, run `xcodegen generate` before anything else.

**Carry the container through every command.** A repo holding both a `.xcworkspace`
and a `.xcodeproj` makes bare `xcodebuild` ambiguous, and it will pick the wrong one
or fail:

```bash
if   ls *.xcworkspace >/dev/null 2>&1; then XC=(-workspace "$(ls -d *.xcworkspace | head -1)")
elif ls *.xcodeproj   >/dev/null 2>&1; then XC=(-project   "$(ls -d *.xcodeproj   | head -1)")
else XC=(); fi
xcodebuild "${XC[@]}" -list -json
```

Every later `xcodebuild` call takes `"${XC[@]}"`. One scheme is the answer; several,
ask once.

Split macOS from iOS by the scheme's own settings, never by guessing:

```bash
xcodebuild "${XC[@]}" -showBuildSettings -scheme "$SCHEME" -configuration Debug 2>/dev/null \
  | grep -E '^ +SUPPORTED_PLATFORMS '
```

`macosx` → macOS. `iphonesimulator` → iOS. Both → ask which one the fix is in.

**Pin the destination, then re-read the settings through it.** `BUILT_PRODUCTS_DIR`
is platform-specific (`Debug` vs `Debug-iphonesimulator`); read without a destination
it answers for the scheme's *default* platform, so the launch would look in a
directory nothing was written to — and the proof step would validate the wrong place.
For iOS, pin one simulator by UDID: `booted` is ambiguous with several up, and a
`name=` destination need not be the one that is booted.

```bash
# iOS only — choose exactly one device and use it for build, install and launch
UDID=$(xcrun simctl list devices booted -j | python3 -c 'import json,sys;d=json.load(sys.stdin)["devices"];print(next((x["udid"] for v in d.values() for x in v),""))')
[ -z "$UDID" ] && { UDID=$(xcrun simctl list devices available -j | python3 -c 'import json,sys;d=json.load(sys.stdin)["devices"];print(next((x["udid"] for v in d.values() for x in v if "iPhone" in x["name"]),""))'); xcrun simctl boot "$UDID"; }
DEST="platform=iOS Simulator,id=$UDID"      # macOS: DEST='platform=macOS'

eval "$(xcodebuild "${XC[@]}" -showBuildSettings -scheme "$SCHEME" -configuration Debug -destination "$DEST" 2>/dev/null \
  | awk -F' = ' '/^ +(BUILT_PRODUCTS_DIR|FULL_PRODUCT_NAME|PRODUCT_BUNDLE_IDENTIFIER|EXECUTABLE_NAME) /{gsub(/^ +/,"",$1); print $1"=\""$2"\""}')"
```

All four values are used below, and **the launch phase refuses to run without them**.

### Non-Xcode projects

- **SwiftPM with a GUI app bundle** — build with `swift build`, then treat the
  produced `.app` exactly as below. A plain **command-line** executable has no bundle,
  no `PRODUCT_BUNDLE_IDENTIFIER` and nothing to look at: do not force it through this
  flow. Run it once, show its output, and say that tests are the real check.
- **Web** — see Phase 6b.

## Phase 3 — the stale-app census (do not skip)

This is the phase that addresses the actual complaint. Before building, find every
bundle the user could plausibly be looking at:

```bash
# 1. What Spotlight, the Dock and `open -a` would actually start. Ask LaunchServices
#    BY BUNDLE ID — `path to application "<name>"` matches the *display* name, which
#    need not equal the executable name, and a silent miss defeats the census. Do not
#    probe with `open -Ra` either: it pops a Finder window as a side effect.
osascript -e "POSIX path of (path to application id \"$PRODUCT_BUNDLE_IDENTIFIER\")" 2>/dev/null
# 2. Build products lying around. Spotlight does not index DerivedData, so `mdfind`
#    finds none of these — glob the directory instead.
ls -d ~/Library/Developer/Xcode/DerivedData/*/Build/Products/*/"$FULL_PRODUCT_NAME" 2>/dev/null
# 3. Anything of this app already running, wherever it came from.
pgrep -fl "/Contents/MacOS/${EXECUTABLE_NAME}$" 2>/dev/null
```

Expect several — a project rebuilt under different DerivedData hashes accumulates
them. Report each with its build time (`date -r "<app>/Contents/MacOS/<exec>"`), then
say plainly: *"the one I am about to build and open is `<BUILT_PRODUCTS_DIR>/<name>`;
the copy in /Applications is from `<date>` and does not have this fix."* One sentence
— this is the misunderstanding that sends people hunting a bug that is not there. If
no other copy exists, say nothing; do not narrate an ambiguity that is absent.

## Phase 4 — build the branch that is checked out

Record the commit you are about to build: `VERIFIED_HEAD=$(git rev-parse HEAD)`. Phase 5
names it as the commit on screen, and a yes in Phase 6 is about this commit only.

```bash
xcodebuild "${XC[@]}" -scheme "$SCHEME" -configuration Debug -destination "$DEST" build
```

For iOS, `mcp__XcodeBuildMCP__build_run_sim` does build, install and launch in one
step — check availability with `ToolSearch` first, since these tools load on demand.
The CLI equivalent installs and launches **on the same UDID** that was built for:
`xcrun simctl install "$UDID" "$BUILT_PRODUCTS_DIR/$FULL_PRODUCT_NAME"` then
`xcrun simctl launch "$UDID" "$PRODUCT_BUNDLE_IDENTIFIER"`.

A failing build ends the skill here. Report the first real error, fix it if it is
within the current task, and never proceed to a launch that would show stale code.

A build can also exit 0 without leaving the product where the settings said it would
be. Check before launching — `open` on a missing path raises a macOS dialog that
explains nothing:

```bash
[ -d "$BUILT_PRODUCTS_DIR/$FULL_PRODUCT_NAME" ] || { echo "build succeeded but $FULL_PRODUCT_NAME is not in $BUILT_PRODUCTS_DIR"; exit 1; }
```

## Phase 5 — launch that exact bundle, and prove it

**Refuse to run this phase with an unset variable.** The first line is not
decoration: an empty `$EXECUTABLE_NAME` would turn the fallback below into
`pkill -f "/Contents/MacOS/"`, which matches every bundled process on the machine —
`loginwindow` included — and would log the user out. Measured, not theorised.

```bash
: "${EXECUTABLE_NAME:?resolve build settings in Phase 2 first}" \
  "${PRODUCT_BUNDLE_IDENTIFIER:?resolve build settings in Phase 2 first}" \
  "${BUILT_PRODUCTS_DIR:?resolve build settings in Phase 2 first}"
```

Quit any running instance — otherwise macOS activates the copy already up, very often
the wrong one — and **wait for it to actually exit**, because both quit paths return
before the process does:

```bash
if pgrep -f "/Contents/MacOS/${EXECUTABLE_NAME}$" >/dev/null; then
  osascript -e "tell application id \"$PRODUCT_BUNDLE_IDENTIFIER\" to quit" 2>/dev/null \
    || pkill -f "/Contents/MacOS/${EXECUTABLE_NAME}$"
  for _ in $(seq 1 20); do
    pgrep -f "/Contents/MacOS/${EXECUTABLE_NAME}$" >/dev/null || break; sleep 0.25
  done
fi
open "$BUILT_PRODUCTS_DIR/$FULL_PRODUCT_NAME"
```

Then **prove** what came up rather than assuming — and wait for it, since a cold start
with signature validation takes well over a second. Match on the **full executable
path**, not a process name, so a helper or another copy cannot answer for it:

```bash
WANT="$BUILT_PRODUCTS_DIR/$FULL_PRODUCT_NAME/Contents/MacOS/$EXECUTABLE_NAME"
PID=""
for _ in $(seq 1 40); do
  PID=$(pgrep -n "$EXECUTABLE_NAME" 2>/dev/null || true)
  [ -n "$PID" ] && [ "$(ps -o comm= -p "$PID" 2>/dev/null)" = "$WANT" ] && break
  PID=""; sleep 0.25
done
[ -n "$PID" ] && echo "on screen: $(ps -o comm= -p "$PID")" || echo "the branch build did not come up within 10s"
```

Match by **string equality on the full path**, not by a pattern: `pgrep -f` treats
its argument as a regular expression, and a bundle path is not one — a `+` or `(`
anywhere in it would quietly stop the proof from ever matching.

If nothing appeared, that is a launch failure — **not** a verdict on the fix. Report
it and stop. If a different copy is running instead, quit it, wait, and relaunch.

**iOS needs its own proof**, for the same reason: the simulator may still hold an
older install of the same bundle id, and `launch` will happily start that one.

```bash
xcrun simctl get_app_container "$UDID" "$PRODUCT_BUNDLE_IDENTIFIER" app
stat -f '%Sm' "$(xcrun simctl get_app_container "$UDID" "$PRODUCT_BUNDLE_IDENTIFIER" app)"
```

The timestamp must be from this run. If it is older the install did not take:
`xcrun simctl uninstall "$UDID" "$PRODUCT_BUNDLE_IDENTIFIER"`, install again.

Report one line: which branch, which bundle path, and which commit is on screen.

## Phase 6 — the human gate

The user is the instrument; a person looking is the entire point. Ask with
`AskUserQuestion`, naming the behaviour from Phase 1:

- **Yes, it works** — asked per observable change when there are several; only when
  all of them hold → Phase 7. Phase 7 then lands without asking again, so settle now
  what a yes does, and say it in this option:
  - Name `VERIFIED_HEAD`, recorded in Phase 4 before the build: the commit the user is
    looking at. Do not re-read `HEAD` here; a commit made since the build is not on screen.
  - Read the landing mode (the rule in Phase 7, step 2) and name what follows: `solo` —
    push, then land on the default branch through `/superpowers-gstack:land`; `pr` —
    push, then open a pull request through `/ship`; no valid line — push, then `land`
    asks once how this project lands. With no remote configured, say that nothing is
    pushed and the landing stays on this machine. If `/review` has not run on this
    branch in this session, say that a yes lands code no review has seen (`land`
    assumes one has).
  - When Phase 0 skipped or blocked Phase 7 (the default branch, a detached HEAD, or
    uncommitted changes the user declined to commit), say instead that a yes lands
    nothing.
- **No, it still behaves the old way** → stay on the branch, land nothing. Say what
  you would check next and offer `/investigate`.
- **Something else broke** → same: land nothing, investigate.

**End your message at this question.** Do not pre-run the landing steps and do not
describe them as though they had happened.

### Phase 6b — the web track, briefly

Record `VERIFIED_HEAD=$(git rev-parse HEAD)` first, as Phase 4 does. Then start the
project's dev script **and keep its PID**. A ready port does not prove the
server is yours: an older one from another branch or worktree may already own it, in
which case the new process exits and the readiness probe succeeds against stale code
— the same bug as the stale bundle, wearing a different hat. Check that the process
you started is still alive before trusting the port, and if the port was taken, say
so and use a free one. Then the same gate, unchanged. Skip Phase 3 — a dev server
serves the working tree, so no stale-bundle ambiguity exists.

## Phase 7 — land it

Landing has two halves and they are not the same: **pushing is backup, landing is
completion.** Do both, in that order. The yes in Phase 6 is the one human review; Phase 7
adds no question of its own when the project has already decided how it lands.

0. **Check that the yes still applies.** A yes covers the build the user saw and the
   landing they were told about, nothing else. If `git rev-parse HEAD` is no longer
   `VERIFIED_HEAD`, or the landing mode now reads differently from what the Phase 6
   option said, land nothing and say what changed. A new commit means a new build: go
   back to Phase 4. A changed mode only invalidates the announcement: go back to Phase 6
   and ask again with the mode as it now reads.
1. **Push first**, so the verified work exists in more than one place whatever is
   decided next. Pass the branch as a quoted argument — refs may legally contain
   `$( )`, `;` and `&`:
   ```bash
   branch=$(git symbolic-ref --short HEAD)
   git rev-parse -q --verify "@{upstream}" >/dev/null \
     && git push \
     || git push -u origin -- "$branch"
   ```
   If the push fails, land nothing: report the error and stop, so landing never runs
   without the backup the Phase 6 option promised. With no remote configured there is
   nothing to push: say plainly that the work stays on this machine, then go on.
2. **Then land it by the project's landing mode.** Read the `Landing mode:` line the way
   `land` does: the exact line `Landing mode: solo` or `Landing mode: pr` in the
   `CLAUDE.md` at the root of this worktree (`git rev-parse --show-toplevel`), a real line
   outside fenced code blocks. Compare it with the same line on the default branch
   (`git show "$DEFAULT_REF":CLAUDE.md`, the ref from Phase 1). When `CLAUDE.md` is a
   symlink, that prints only the link's target path: read the target the same way
   (`git show "$DEFAULT_REF":AGENTS.md` for a link to `AGENTS.md`), as `land` does.
   - **The branch changes the landing mode** (the two differ) → land nothing
     automatically: a branch must not set the policy it is landed under. Ask once with
     `AskUserQuestion` which mode holds, naming both values, and end your message at it.
     Act on the answer as `land`'s exit-14 row says: it is the same stop, made by `land`
     itself when it is invoked on such a branch.
   - **`solo`** → invoke `/superpowers-gstack:land` for this worktree, from its root, with
     no further question.
   - **`pr`** → invoke `/ship` inside this worktree, with no further question from this
     skill. `/ship` may ask its own questions about the pull request; those are not a
     second ask of the landing.
   - **No valid line** (missing, only inside a code block, or both values) → invoke
     `/superpowers-gstack:land` anyway. Its exit-2 row owns this question: it asks the
     user once (solo or pull request? or, with both values, which one holds) and records
     the answer. Do not ask it here as well.

   When `land` stops with an exit code, follow `land`'s own exit-code table: it is the
   authority on the next step. Add no menu of your own on top of it, and never offer
   `/superpowers:finishing-a-development-branch` from this skill: it merges without the
   project's pre-merge gate, which is why `land` stops at exit 8 when `wt` is not
   installed instead of falling back to it.

**If the branch is checked out in a worktree**, run `/ship` and `/superpowers-gstack:land`
from that worktree's root: a branch cannot be checked out twice, and `git checkout` fails
outright elsewhere (`git worktree list` gives the path). The landing skills move the
default branch themselves; do not hand-roll a merge.

After `land` exits 0, follow its `remaining` field: it prints the cleanup commands in the
order git needs them (the worktree first, then the branch).

## What this skill is not

- Not a test run. Tests answer "did I break something else"; this answers "is the
  thing I fixed actually fixed". `/ship` runs the tests on the way to a PR.
- Not UI automation. `/superpowers-gstack:e2e-route` owns automated E2E; this is the
  one case where a human eye is the point.
- Not a release. It builds Debug and launches locally; shipping to users is
  `/land-and-deploy`.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.