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-shipCandid Ship orchestrates the full shipping workflow:
- Run
candid-loopto review and fix code issues - Install dependencies (when
installCommandis set) - Execute your build command
- Run your tests
- Create a pull request
- 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: disabledBefore executing, the plan is shown for confirmation. Use --dry-run to preview without executing.
Pre-Flight Checks
Before starting, candid-ship validates:
- GitHub CLI —
ghis 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
| Flag | Description | Default |
|---|---|---|
--auto-merge | Enable auto-merge after PR creation | from config |
--no-auto-merge | Disable auto-merge even if config enables it | from config |
--skip-review | Skip the candid-loop review step | false |
--skip-install | Skip dependency install | false |
--skip-build | Skip build verification | false |
--skip-tests | Skip test execution | false |
--dry-run | Show plan without executing | false |
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"
}
}| Field | Type | Default | Description |
|---|---|---|---|
installCommand | string | — | Shell command to install dependencies. Runs before build/tests. Skipped if not set. Common values: pnpm install, npm ci, poetry install. |
buildCommand | string | — | Shell command for build verification. Skipped if not set. |
testCommand | string | — | Shell command for running tests. Skipped if not set. |
targetBranch | string | first mergeTargetBranches or "main" | Branch to target for the PR. |
autoMerge | boolean | false | Auto-merge the PR after creation. |
additionalPrompt | string | — | Extra context passed to candid-loop/review during the review step. |
postMergeCommand | string | — | Shell 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:
| Stack | Install command |
|---|---|
| pnpm | pnpm install |
| npm (CI-friendly) | npm ci |
| npm | npm install |
| yarn | yarn install --frozen-lockfile |
| poetry | poetry install |
| bundler | bundle install |
| go | go 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 --autoThis 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:
postMergeCommandis configuredautoMergeis enabledgh pr merge --squash --autosucceeded
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
- After the PR is created, candid-ship reads the current branch name (
git branch --show-current). - It builds a case-insensitive regex from
issueTracker.teamPrefixes— for example,(DIS|ENG|DISC)-\d+. - 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 toissueTracker.state(default:"In Review"). - 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."
}
}
}| Field | Type | Default | Description |
|---|---|---|---|
provider | string | "linear" | Issue tracker. Only "linear" is supported today. |
enabled | boolean | false | Opt-in flag. Requires the provider’s MCP. |
teamPrefixes | string[] | ["DIS", "ENG", "DISC"] | Team keys to match in branch names. Edit to match your tracker workspace’s prefixes. |
state | string | "In Review" | Workflow state to transition the issue to. Must match a state name in your tracker exactly. |
prompt | string | (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 name | Matched issue |
|---|---|
ron-myers/dis-509-add-auth | DIS-509 |
feature/ENG-1234-fix-login | ENG-1234 |
disc-42 | DISC-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:
- Single issue — only
{issueId}is touched, never any other issue. - Single field — only the
statefield changes (assignees, labels, priority, title, description are untouched). - Idempotent — already in
{state}? Succeed without action. - 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:
| Placeholder | Substituted with | Example |
|---|---|---|
{issueId} | The matched issue ID (uppercase) | DIS-509 |
{state} | Value of issueTracker.state | In Review |
{provider} | Value of issueTracker.provider | linear |
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.
| Scenario | Output |
|---|---|
issueTracker field absent from config | No issue tracker configured — skipping |
enabled is false | Skipping issue tracker update (disabled) |
provider is "none" or unset | Skipping issue tracker update (no provider set) |
provider is unsupported (e.g. "asana") | Warning + link to GitHub issues , step skipped |
| Provider’s MCP isn’t installed | Linear MCP not available — skipping issue update |
| Branch name has no team prefix match | No 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:
- Open
.candid/config.json. - Add the new team key to
ship.issueTracker.teamPrefixes. - Save. The next
/candid-shipwill 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-mergeOutput 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: PASSSuccess (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 enabledWith Skipped Steps
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Candid Ship Complete
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Review: SKIPPED
Build: PASS
Tests: SKIPPED
PR: https://github.com/org/repo/pull/42
Merge: Manual merge requiredBuild 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
loopconfig - 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
- Candid Fast Ship — skip review and tests for low-risk changes
- Candid Loop — run review repeatedly until all issues are resolved
- Candid Chrome QA — catch visual and UX bugs before shipping
- Tone Selection — control how direct or gentle review feedback is