Skip to Content

Candid Ship

Ship your branch with one command — review, build, test, PR, and merge.

Looking for ready-to-paste config examples? See the Ship Configuration Reference — recipes for minimal ship, full pipeline, hands-off auto-merge, Linear integration, and post-merge deploy hooks.

Quick Start

/candid-ship

Candid Ship orchestrates the full shipping workflow:

  1. Run candid-loop to review and fix code issues
  2. Install dependencies (when installCommand is set)
  3. Execute your build command
  4. Run your tests
  5. Create a pull request
  6. Optionally auto-merge

Any failure before PR creation aborts immediately with a clear message.

How It Works

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Candid Ship Plan
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Branch: feature/add-auth → stable

Steps:
  1. 🔍 Review code (candid-loop)
  2. 🛠️  Install: pnpm install
  3. 🔨 Build: npm run build
  4. 🧪 Tests: npm test
  5. 📋 Create pull request
  6. 🔀 Auto-merge: disabled

Before executing, the plan is shown for confirmation. Use --dry-run to preview without executing.

Pre-Flight Checks

Before starting, candid-ship validates:

  • GitHub CLIgh is installed and authenticated
  • Git repository — you’re inside a git repo
  • Branch state — you’re on a feature branch (not the target branch)
  • Commits ahead — your branch has commits beyond the target

If any check fails, you get a clear error message before any work begins.

Configuration

CLI Flags

FlagDescriptionDefault
--auto-mergeEnable auto-merge after PR creationfrom config
--no-auto-mergeDisable auto-merge even if config enables itfrom config
--skip-reviewSkip the candid-loop review stepfalse
--skip-installSkip dependency installfalse
--skip-buildSkip build verificationfalse
--skip-testsSkip test executionfalse
--dry-runShow plan without executingfalse

Config File

Add to .candid/config.json:

{
  "ship": {
    "installCommand": "pnpm install",
    "buildCommand": "npm run build",
    "testCommand": "npm test",
    "targetBranch": "stable",
    "autoMerge": false,
    "additionalPrompt": "Focus on security and ensure all API endpoints have auth middleware",
    "postMergeCommand": "curl -X POST https://deploy.example.com/trigger"
  }
}
FieldTypeDefaultDescription
installCommandstringShell command to install dependencies. Runs before build/tests. Skipped if not set. Common values: pnpm install, npm ci, poetry install.
buildCommandstringShell command for build verification. Skipped if not set.
testCommandstringShell command for running tests. Skipped if not set.
targetBranchstringfirst mergeTargetBranches or "main"Branch to target for the PR.
autoMergebooleanfalseAuto-merge the PR after creation.
additionalPromptstringExtra context passed to candid-loop/review during the review step.
postMergeCommandstringShell command to run after auto-merge succeeds. Skipped if auto-merge is disabled or fails.

Install Step

When installCommand is set, candid-ship runs it before the build and test steps. This avoids the common failure mode where build/test errors are really stale-dependency errors.

{
  "ship": {
    "installCommand": "pnpm install",
    "buildCommand": "pnpm build",
    "testCommand": "pnpm test"
  }
}

The install step is opt-in — leave installCommand unset to skip it entirely. If your build/test commands already handle their own install (e.g. buildCommand: "pnpm install && pnpm build"), you can keep that pattern; the dedicated installCommand field just makes the install phase visible and independently skippable via --skip-install.

Common values:

StackInstall command
pnpmpnpm install
npm (CI-friendly)npm ci
npmnpm install
yarnyarn install --frozen-lockfile
poetrypoetry install
bundlerbundle install
gogo mod download

If install fails, the ship aborts before review/build/tests run — giving you a clear, focused error.

Additional Prompt

The additionalPrompt field lets you add review context that’s specific to your shipping workflow. This text is passed to candid-loop which forwards it to candid-review.

{
  "ship": {
    "additionalPrompt": "Focus on security and ensure all API endpoints have auth middleware"
  }
}

This is useful for enforcing team-wide shipping standards beyond what’s in your Technical.md.

Auto-Merge

When autoMerge is enabled, candid-ship uses GitHub’s auto-merge feature:

gh pr merge --squash --auto

This means the PR will merge automatically once all branch protection checks pass. If your repository doesn’t have auto-merge enabled, you’ll see a warning but the PR is still created.

Note: Auto-merge requires the “Allow auto-merge” setting to be enabled in your GitHub repository settings under Settings → General → Pull Requests.

Post-Merge Command

When postMergeCommand is configured, candid-ship runs the specified shell command after auto-merge is successfully enabled. This is useful for triggering deployments, sending notifications, or running cleanup scripts.

{
  "ship": {
    "autoMerge": true,
    "postMergeCommand": "curl -X POST https://deploy.example.com/trigger"
  }
}

The command only runs when all three conditions are met:

  • postMergeCommand is configured
  • autoMerge is enabled
  • gh pr merge --squash --auto succeeded

If the command fails, a warning is shown but the workflow is not aborted — the PR is already created and auto-merge is in progress.

Issue Tracker Integration

When issueTracker.enabled is true, candid-ship reads the current branch name, extracts an issue ID, and updates that issue’s state in your tracker right after creating the PR. New in v1.13.0.

Provider support: Linear is the only supported provider today. The config is provider-agnostic and built to extend — see requesting support below.

How it works

  1. After the PR is created, candid-ship reads the current branch name (git branch --show-current).
  2. It builds a case-insensitive regex from issueTracker.teamPrefixes — for example, (DIS|ENG|DISC)-\d+.
  3. If the branch name contains a match, candid-ship renders the prompt template (substituting {issueId}, {state}, {provider}) and calls the provider’s MCP server to update only that single issue to issueTracker.state (default: "In Review").
  4. The result appears in the ship summary as Issue: Updated DIS-509 → In Review.

If no issue ID is found in the branch name, the step is skipped silently — branches without a tracked issue just ship normally. The same is true if the tracker isn’t configured at all.

Configuration

{
  "ship": {
    "issueTracker": {
      "provider": "linear",
      "enabled": true,
      "teamPrefixes": ["DIS", "ENG", "DISC"],
      "state": "In Review",
      "prompt": "Update issue {issueId}: set its state to \"{state}\". Update only this one issue and only its state — do not modify any other issues, fields, or properties. If the issue is already in \"{state}\", report success without action. If the issue is missing or inaccessible, report the error and stop."
    }
  }
}
FieldTypeDefaultDescription
providerstring"linear"Issue tracker. Only "linear" is supported today.
enabledbooleanfalseOpt-in flag. Requires the provider’s MCP.
teamPrefixesstring[]["DIS", "ENG", "DISC"]Team keys to match in branch names. Edit to match your tracker workspace’s prefixes.
statestring"In Review"Workflow state to transition the issue to. Must match a state name in your tracker exactly.
promptstring(see default)Customizable prompt template sent to the MCP. Supports {issueId}, {state}, {provider} placeholders. Must restrict the action to a single issue.

Branch naming

Use a branch name that includes the issue ID anywhere in the name. The match is case-insensitive:

Branch nameMatched issue
ron-myers/dis-509-add-authDIS-509
feature/ENG-1234-fix-loginENG-1234
disc-42DISC-42
main(no match — skipped)
feature/refactor-auth(no match — skipped)

A common convention:

[your-name]/[issue-id]-[short-description]

Linear’s “Copy git branch name” feature on each issue produces a perfectly formatted branch name with the issue ID baked in.

Customizing the prompt

The default prompt is written into .candid/config.json by candid-init so it’s visible and easy to edit:

Update issue {issueId}: set its state to "{state}". Update only this one issue and only its state — do not modify any other issues, fields, or properties. If the issue is already in "{state}", report success without action. If the issue is missing or inaccessible, report the error and stop.

The default encodes four safety invariants so a verbose MCP can’t accidentally fan out:

  1. Single issue — only {issueId} is touched, never any other issue.
  2. Single field — only the state field changes (assignees, labels, priority, title, description are untouched).
  3. Idempotent — already in {state}? Succeed without action.
  4. No fallback search — missing or inaccessible issue? Stop. Don’t look for “similar” issues.

You can customize this to add domain-specific instructions (e.g. add a comment, set an assignee, link to the PR), but any custom prompt must preserve invariants 1 and 4 — single issue, no fallback search. Without those, a chatty MCP could fan out and modify unrelated issues.

Example custom prompt:

{
  "ship": {
    "issueTracker": {
      "provider": "linear",
      "enabled": true,
      "state": "Code Review",
      "prompt": "Move {issueId} to \"{state}\" and add a comment 'PR ready for review.' Only modify this single issue."
    }
  }
}

Available placeholders:

PlaceholderSubstituted withExample
{issueId}The matched issue ID (uppercase)DIS-509
{state}Value of issueTracker.stateIn Review
{provider}Value of issueTracker.providerlinear

Setup: Linear MCP

The Linear MCP server is required when provider: "linear". If it’s not installed, the issue-tracker step is skipped silently — the rest of the ship still works.

Step 1 — Install the Linear MCP. From your Claude Code settings, add the official Linear MCP server (provided by claude.ai). When prompted, sign in with the Linear account that has access to the issues you want to update.

Step 2 — Verify the connection. In a fresh Claude Code conversation, ask Claude to list your Linear teams. If Claude returns your team list, the MCP is wired up correctly.

Step 3 — Find your team prefixes. Open Linear and look at any issue ID. The letters before the dash are your team key (e.g. DIS-509 → team key DIS). Note every team key whose issues you ship from this repository.

Step 4 — Set teamPrefixes in your config. Add an entry to .candid/config.json:

{
  "ship": {
    "issueTracker": {
      "provider": "linear",
      "enabled": true,
      "teamPrefixes": ["YOUR", "TEAM", "KEYS"]
    }
  }
}

Step 5 — Confirm the state name. Open an issue in Linear and check the workflow state names in the right sidebar. The state field must match exactly. Common values:

  • "In Review"
  • "Code Review"
  • "In Progress"
  • "Ready for Review"

If the state name has different capitalization or wording, the update will fail with a state not found error — you’ll see a warning, the PR is still created.

When the update is skipped

candid-ship skips the issue-tracker update gracefully in every case below. The ship continues regardless — the issue tracker is purely additive.

ScenarioOutput
issueTracker field absent from configNo issue tracker configured — skipping
enabled is falseSkipping issue tracker update (disabled)
provider is "none" or unsetSkipping issue tracker update (no provider set)
provider is unsupported (e.g. "asana")Warning + link to GitHub issues , step skipped
Provider’s MCP isn’t installedLinear MCP not available — skipping issue update
Branch name has no team prefix matchNo issue ID found in branch name — skipping
MCP returns an error (issue not found, bad state name, etc.)Warning shown, ship continues. PR is already created.

This means you can safely commit issueTracker config to a repo that other developers ship from — if they don’t have the Linear MCP, their ships still work.

Example flow

Branch: ron-myers/dis-509-add-auth → stable

Step 4/6: Creating pull request...
PR created: https://github.com/org/repo/pull/42

Step 5/6: Updating issue tracker (linear)...
Updated DIS-509 → In Review

Step 6/6: Enabling auto-merge...
Auto-merge enabled. PR will merge when checks pass.

Adding more team prefixes later

Team prefixes are workspace-specific. The defaults (DIS, ENG, DISC) are starting examples — edit them to match your workspace. To add a new team:

  1. Open .candid/config.json.
  2. Add the new team key to ship.issueTracker.teamPrefixes.
  3. Save. The next /candid-ship will recognize branches with that prefix.

No restart, no reinstall — the config is read fresh on every run.

Requesting support for other trackers

Today Linear is the only supported provider. The config schema is built to extend — adding Asana, Jira, GitHub Issues, Shortcut, or any other tracker is a matter of mapping the provider’s MCP into the same provider / teamPrefixes / state / prompt contract.

If you’d like support for another tracker:

Open a request: github.com/ron-myers/candid/issues 

Tell us:

  • Which tracker you use
  • How issue IDs are formatted (e.g. team-prefix conventions)
  • The state/status field name and target state
  • Any tracker-specific quirks (custom fields, multi-step transitions, etc.)

Until your tracker is supported, provider: "your-tracker" produces a friendly warning during ship and the step is skipped — the rest of the ship works normally.

Examples

# Full workflow with config defaults
/candid-ship
 
# Force auto-merge
/candid-ship --auto-merge
 
# Quick ship — skip review, just install/build/test/PR
/candid-ship --skip-review
 
# Skip install (deps already up to date)
/candid-ship --skip-install
 
# Preview the plan without executing
/candid-ship --dry-run
 
# Skip everything except PR creation
/candid-ship --skip-review --skip-install --skip-build --skip-tests
 
# Override auto-merge for this run
/candid-ship --no-auto-merge

Output Examples

Success (with install and post-merge command)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Candid Ship Complete
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Review:   PASS (3 iterations, 7 issues fixed)
Install:  PASS
Build:    PASS
Tests:    PASS
PR:       https://github.com/org/repo/pull/42
Merge:    Auto-merge enabled
Post-merge: PASS

Success (without install or post-merge command)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Candid Ship Complete
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Review:   PASS (3 iterations, 7 issues fixed)
Build:    PASS
Tests:    PASS
PR:       https://github.com/org/repo/pull/42
Merge:    Auto-merge enabled

With Skipped Steps

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Candid Ship Complete
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Review:   SKIPPED
Build:    PASS
Tests:    SKIPPED
PR:       https://github.com/org/repo/pull/42
Merge:    Manual merge required

Build Failure

Step 3/6: Running build...
$ npm run build

[build output with errors]

Build failed. Ship aborted.

Combining with Other Features

Candid Ship respects your other Candid settings:

  • Tone — the review step uses your configured tone
  • Loop settings — max iterations, enforce categories, and ignored issues from your loop config
  • Decision register — prior decisions are applied during the review step
  • Exclude patterns — files excluded from review carry through

Workflow Example

A typical shipping workflow with candid-ship configured:

{
  "version": 1,
  "tone": "constructive",
  "mergeTargetBranches": ["stable"],
  "autoCommit": true,
  "ship": {
    "installCommand": "pnpm install",
    "buildCommand": "pnpm build",
    "testCommand": "pnpm test",
    "targetBranch": "stable",
    "autoMerge": true,
    "additionalPrompt": "Ensure error handling covers all async operations",
    "postMergeCommand": "echo 'Shipped successfully!'"
  }
}

Then just run /candid-ship and your code goes from review to merged PR.

See Also

Last updated on