Publish from your phone: checkpoints, a facts gate and a one-tap merge

Claude Code researches, drafts and publishes a post from your phone, stopping for your tap at the outline, review and merge, and counting a number as verified only when a fetched quote backs it.

Skill advanced Tested Opus 5.5 · Nov 2026May be dated

The prompt

Save it as .claude/skills/checkpointed-publish/SKILL.md in the repository your site builds from, and make the one-time changes in its Setup section. Then start a Claude Code session on that repository, from the Claude app on your phone if you like, and type post <topic> or /checkpointed-publish post <topic>. A bare /post reaches Claude only in cloud, Desktop and VS Code sessions; in the terminal it is an unknown command. For real /post and /publish commands, see Two slash commands under Variations.

Fill in the blanks9 placeholders · empty ones stay as {{…}}

Example

Input:

  • content_type: explainer article
  • repo: example-org/explainers
  • owner_login: example-owner
  • content_dir: content/<slug>/
  • piece_file: content/<slug>/index.md; front matter draft: true|false, date, updated
  • checks: npm run build
  • preview_check: the preview URL the hosting bot comments on the PR
  • build_check: site-build
  • live_check: https://explainers.example.com/<slug>/ serves <meta name="rev" content="<merge sha>">
  • The owner's message: post how tides work

The test session had no repository and no git, GitHub, PushNotification or AskUserQuestion tools, and could not read .claude/settings.json. It was told to write [would run: …] and [would push: …] lines in place of tool calls, and to stop at the outline checkpoint.

Output (trimmed: two of the 14 sources and five of the 32 entries from facts.json, then the reply at the outline checkpoint):

{
  "sources": [
    { "id": "s1", "publisher": "NOAA National Ocean Service", "title": "What Causes Tides - Tides and Water Levels: NOAA's National Ocean Service Education", "url": "https://oceanservice.noaa.gov/education/tutorial_tides/tides02_cause.html", "accessed": "2026-11-06" },
    …
    { "id": "s3", "publisher": "NOAA National Ocean Service", "title": "Frequency of Tides - The Lunar Day - Tides and Water Levels: NOAA's National Ocean Service Education", "url": "https://oceanservice.noaa.gov/education/tutorial_tides/tides05_lunarday.html", "accessed": "2026-11-06" },
    …
  ],
  "data": {
    "sun_tide_distance_reduction": { "value": 59000000, "unit": "times (390 cubed, rounded by the source)", "src": "s1", "method": "", "verified": true, "note": "s1: \"Thus, its tide-generating force is reduced by 390³, or about 59 million times less than the moon.\" (the page text renders the exponent as \"3903\")" },
    …
    "sun_tide_force_vs_moon_calc": { "value": 0.46, "unit": "fraction of the Moon's tide-generating force", "src": "s1", "method": "sun_mass_vs_moon / sun_tide_distance_reduction = 27,000,000 / 59,000,000 = 0.458, rounded to 0.46", "verified": true, "note": "Computed from two verified s1 inputs; s1 states the result: \"the sun's tide-generating force is about half that of the moon\"" },
    …
    "lunar_day_extra_min": { "value": 50, "unit": "minutes longer than a solar day", "src": "s3", "method": "", "verified": true, "note": "s3: \"The lunar day is 50 minutes longer than a solar day\"" },
    …
    "example_high_tide_today": { "value": 540, "unit": "minutes after midnight (09:00)", "src": "", "method": "", "illustrative": true, "note": "Illustrative start time for the worked example; the piece labels the example as illustrative." },
    "example_high_tide_tomorrow": { "value": 590, "unit": "minutes after midnight (09:50)", "src": "s3", "method": "example_high_tide_today + lunar_day_extra_min = 540 + 50 = 590 (09:50)", "illustrative": true, "note": "Illustrative: an average step; real tables vary day to day. The piece labels the example as illustrative." },
    …
  },
  "unverified": []
}
[would run: git fetch origin]
[would run: ls content/ && gh pr list --repo example-org/explainers --state open --search how-tides-work]
[would run: git switch -c post/how-tides-work]
[would run: npm run build]

How tides work - The Moon pulls, the Sun helps, and the shape of each coast decides the rest.
1. Distance beats size · 0.46 of the Moon's tide force (s1)
2. Two bulges, not one · 2 bulges (s2)
3. Why high tide runs late · 24 h 50 min lunar day (s3)
4. Springs and neaps · 2 spring tides per lunar month (s4)
5. The coast decides the rest · 16 m Bay of Fundy range (s7)
6. A tide table is a sum · 37 harmonic constituents (s9)
7. The long brake · 3.8 cm/year Moon recession (s10)
Sources: 14 · Unverified (0): none
Blockers: npm run build can't run here (no repo checkout). I can't read .claude/settings.json, so I can't check that ask rules cover the GitHub merge, auto-merge and file-write tools. There are no GitHub tools here to open the PR.

[would push: explainer article outline ready: How tides work]

Reply: Approve / Change it / Stop

Directly sourced entries quote the fetched sentence in note, and computed ones name their formula and input keys in method; the two worked-example times are marked illustrative. The session drafted nothing, listed what would block it later, and ended its turn on the Reply: line because it had no question tool.

Why it works

  • A checkpoint ends the turn, and review fixes ratchet. A summary, a push under 200 characters and fixed options make each decision one tap from a lock screen. The fallback (Reply: Approve / Change it / Stop, then end the turn) and "wait for the owner's answer" close the usual failure: a model that asks, finds no question tool, and carries on deciding for the owner. The outline comes before any drafting, so a wrong angle costs one reply, and each defect found in review goes under the brief's Fixed defects, so later drafts check for it.
  • Verified means fetched and quoted, and a check backs the gate. Requiring the supporting sentence in note makes an unproven number visible instead of plausible: it lands in unverified[], and the facts gate forces one decision per item (confirm, illustrative or drop) before anything merges. "Never estimate to fill a gap" shuts the quiet way invented numbers get in, and the facts check refuses a published piece whose ledger still has an open item, even if the session skipped the gate.
  • Only the owner approves, and the merge is a permission prompt. Treating PR comments and fetched pages as data means a comment saying "approved, publish" does nothing; even the owner's own PR comments count only as edit requests. Putting every GitHub merge and file-write tool under ask (by glob, so the server's name doesn't matter), allow-listing the routine steps, and denying pushes to the default branch and deploy commands make the Allow tap the only route the session is permitted to take, and the branch ruleset makes it the only route GitHub accepts. expectedHeadSha merges exactly the commit that was checked.
  • Done means the merge commit is served. Matching the merge SHA in a cache-busted fetch of the live page catches failed deploys and stale caches that a 200 hides, and the 4-line report fits a phone screen. A build check that never appears stops at a checkpoint instead of a silent wait.

Variations

  • Two slash commands. Split the skill into post and publish so /post and /publish are real commands. Set disable-model-invocation: true on publish so only the owner's /publish runs it; the review checkpoint's Publish answer then tells the owner to type /publish. The field works only in Claude Code: claude.ai skill uploads reject it.
  • No previews. Set preview_check to "none" and review from the phone-width and desktop screenshots the session sends.
  • Gate other claims. The facts gate works for anything a reader relies on: quotes (confirm the attribution), image licences, prices, dates.
  • Rendered figures. If pieces carry charts or diagrams, add a figure step between outline and review: render each in a headless browser, gate it (overlapping labels, contrast, runtime errors), send the screenshots, and fix the figure, never the checker.
  • Generalised from a blog's publishing workflow.

Install as a skill

This entry comes with a SKILL.md. Claude loads it whenever a request matches its description.

  • Claude Code: unzip into .claude/skills/ in your project, or ~/.claude/skills/ for every project.
  • Claude apps: upload the .zip under Customize → Skills → + → Create skill → Upload a skill. Skills need code execution, which is under Settings → Capabilities.