In the last post I covered /ccmagic:auto-ticket, the autonomous ticket driver in my ccmagic Claude Code plugin: give it a ticket and it implements, reviews, works the PR feedback loop, and either merges or parks with a clear note. And in Self-Hosting a Linear-Driven Claude Code Agent with Cyrus I covered getting Cyrus running: a self-hosted worker that watches Linear, and when an issue is assigned to it, spins up a container with a fresh git worktree and runs a Claude Code session against it.
This post is the two of them bolted together. The end state: I add a FullAuto label to a Linear issue and assign it to the agent, then walk away. Some time later the issue is either Done with a merged PR and a run-summary comment, or parked with a comment explaining exactly what human decision it's waiting on. No laptop involved.
Fair warning: this part broke more often than anything else in the project. Nearly every section below exists because something concrete went wrong, and I've numbered the three that cost me the most time.
How the pieces fit
A quick map before the setup:
- Cyrus owns the trigger and the environment. Label routing decides which prompt a session starts with, the container provides the repo worktree, an authenticated
gh, and (this was a late discovery) the official hosted Linear MCP (https://mcp.linear.app/mcp), authed with the workspace OAuth token. - ccmagic owns the workflow. The
FullAutoprompt invokes/ccmagic:auto-ticket {TICKET-ID}, and from there the plugin drives the entire lifecycle. The GitHub half (branch, push, PR, CI, merge) runs throughghexactly as on a laptop. The Linear half runs over one of two transports, which ccmagic picks automatically:- `mcp` (primary): reads and writes Linear directly through
mcp__linear__*tools. Verified end to end on my instance: a FullAuto ticket ran implement, review, merge and Done entirely over MCP. - `prompt-relay` (fallback): for the brief window at session start where Cyrus's non-blocking MCP is still connecting, and for any harness with no Linear MCP at all. The ticket content travels in with the prompt, and all Linear writes collapse into one final message that the harness relays back to Linear as a comment.
- `mcp` (primary): reads and writes Linear directly through
Step 1: ccmagic installed, and a pinned tracker
Two things have to be true of the environment Cyrus runs in: the ccmagic plugin has to be installed, and the tracker has to be pinned to linear.
I install ccmagic once, globally in the container rather than per-checkout. The plugin lives at ~/.cyrus/user-skills-plugin and is registered for every session, so all my repos pick it up without per-repo setup. A per-repo install works too if you only want it on one project.
The tracker pin is one small config file. I use a global one so it covers every repo:
# ~/.claude/ccmagic.local.md (or per-repo at .claude/ccmagic.local.md)
---
tracker: linear
---Pinning tracker: linear matters more in the container than on a laptop. ccmagic normally auto-detects the tracker, but with tracker: auto and an MCP that hasn't finished connecting, an ENG-123-shaped ID is ambiguous between Linear and JIRA. The pin makes detection deterministic. (github_repo is optional, since ccmagic reads it from the git remote, so I leave it out of the global file and only set it per-repo where I want to be explicit.) The same file works unmodified on your laptop.
If you want good autonomous results, this is also the moment to make sure the repo has the context ccmagic leans on: context/conventions.md and the architecture knowledge that /ccmagic:map-codebase generates. The autonomous reviewer grades against these.
Sharp edge #1: the plugin directory has to be readable by the session, at two layers. Cyrus's Claude Code SDK denies reads into the home directory by default, *and* each session runs inside a bubblewrap filesystem sandbox with its own read allowlist. My plugin lives under ~/.cyrus (a home path), and ccmagic skills read support files from their own skill directory at runtime (auto-ticket loads its shared autonomous-contract.md this way). If either layer blocks it, the skill strands mid-run. Granting it in one place isn't enough. I had to do both: add the plugin path to the SDK's allowedDirectories (a build-time patch to Cyrus's EdgeWorker.js, applied the same way as the FullAuto patch in Step 2) and add it to sandbox.filesystem.allowRead in ~/.claude/settings.json.
Step 2: Route the FullAuto label to the auto-ticket prompt
This is where the plan meets stock Cyrus's limits. Cyrus routes issues to prompts by Linear label via labelPrompts in ~/.cyrus/config.json, per repository. But labelPrompts only maps labels onto Cyrus's built-in preset modes (scoper, debugger, builder). There is no config key that binds an arbitrary FullAuto label to a custom prompt template, so the config-only version of this step doesn't exist. I had to patch Cyrus.
The patch is small and build-time: it teaches Cyrus's PromptBuilder.js a global, case-insensitive FullAuto handler that (a) opts the issue out of role routing and (b) forces the ccmagic auto-ticket template (from CYRUS_FULLAUTO_TEMPLATE, else a default path) on any repo, no per-repo config. Two things keep it safe to live with: I pin cyrus-ai to a known-good version so the patch's anchors stay put, and the patch aborts the build if those anchors move, so a version bump can never silently drop the feature. The plugin-read patch from Sharp edge #1 is applied the same way. (If and when this lands upstream in Cyrus, both patches go away.)
The session also needs an allowedTools set that covers the whole run. This is what I grant per repo:
{
"allowedTools": [
"Read", "Edit", "Write", "Bash", "Bash(git:*)", "Bash(gh:*)", "Bash(npm:*)",
"Task", "Skill", "Glob", "Grep", "TodoWrite", "WebFetch", "WebSearch",
"mcp__linear", "mcp__cyrus-tools", "mcp__cyrus-docs"
]
}Sharp edge #2: an `allowedTools` override replaces the platform default rather than extending it. I set a tailored allowedTools list for the repo and Linear MCP access silently vanished, because the default list (which includes mcp__linear) had been replaced wholesale by mine. If you override allowedTools at all, you must explicitly include mcp__linear, or every run falls back to prompt-relay and never writes to Linear directly.
Step 3: The prompt template
The prompt Cyrus builds for a FullAuto session has to do two very specific things, and both exist because of how Claude Code's skill forking works:
First, write the ticket content to a handoff file the forked skill can read.
`/ccmagic:auto-ticket` runs as a forked skill and only sees its own invocation
arguments — not this prompt — so the ticket content must be handed off via a file
in the working directory, which the fork shares.
Write a file named `.ccmagic-ticket.md` in the working directory
({{working_directory}}) with exactly this content:
~~~
{{issue_title}}
{{issue_description}}
~~~
Then run:
/ccmagic:auto-ticket {{issue_identifier}}
Environment (already prepared by Cyrus — do not re-create the branch or worktree):
- Working directory: {{working_directory}}
- Branch: {{branch_name}}
- Base branch: {{base_branch}}
When the command finishes, its output ends with a block delimited by:
=== FINAL MESSAGE TO RELAY (reproduce verbatim) ===
...
=== END FINAL MESSAGE ===
Reproduce the contents of that block verbatim as your own final message.
Do not summarize, paraphrase, or add commentary around it.Cyrus substitutes {{issue_identifier}}, {{issue_title}}, {{issue_description}}, and the {{working_directory}} / {{branch_name}} / {{base_branch}} values from the assigned issue at build time. Why this shape:
The handoff file exists because of the fork boundary. auto-ticket runs as a forked skill (context: fork), which means it sees only its own arguments, never the rest of the prompt. My first template put the issue body in the prompt as sibling text ("the ticket content is above"), and the forked skill simply never received it. Worse, MCP tool discovery has a per-context lag: a forked sub-skill frequently can't yet see tools the top-level session already has. So on a cold start the fork could end up with neither the MCP nor the content, which in an early version meant a stall. The fix: parent and fork share the working directory, so the parent writes the ticket to .ccmagic-ticket.md, and the fork reads it (then deletes it so it's never committed). That way prompt-relay always works, no matter how the MCP connection race turns out.
The verbatim-relay instruction exists because of paraphrasing. Cyrus posts the session's final top-level output to Linear as a comment. But the main-loop model sits between the forked skill's return value and that final output, and left alone it will helpfully summarize. The delimited block and the explicit "reproduce verbatim" instruction are what stop it. The live test below is how I check they still hold.
Step 4: The token lesson
Sharp edge #3: a fine-grained GitHub PAT cannot read check runs authored by GitHub Apps. GitHub Actions is an App. So are bot reviewers like Copilot. So on a Cyrus instance authenticated with a fine-grained PAT, gh pr checks returns Resource not accessible by personal access token (HTTP 403) even with Checks: read granted. My first full smoke test sailed through implement and review, then ended by *asking a human to confirm CI was green*. In an unattended container. That question scrolls into the void.
Two fixes landed. On the ccmagic side, the CI wait and the merge gate now fall back to the APIs a fine-grained token *can* read: the Actions API (gh run list / gh run watch) plus the legacy commit-status API, with green defined strictly (every run completed with a success-class conclusion). And the contract now states flatly that an autonomous run ends by merging or parking, never by emitting a question. On your side: grant the token Actions: read and Commit statuses: read in addition to the usual contents/PR permissions. One caveat: if your required checks are produced by a third-party CI App (not GitHub Actions), no fine-grained token can read them; those runs park as "cannot read CI status" rather than fake a green.
Step 5: Linear automation for the fallback path
Under the mcp transport, ccmagic moves the issue to Done itself. Under prompt-relay it can't touch Linear state; it only emits intent lines like Requested state: Done in the relayed summary. So enable Linear's GitHub integration with auto-close automation, which moves the issue when the PR merges. Without it, a prompt-relay run's merged ticket sits in In Progress forever, looking stuck while being finished. Same logic applies to parking: under prompt-relay the parked ticket's state doesn't change, and the relayed parked comment is the signal. Check comments before concluding nothing happened.
What lands where
A finished run leaves two very different trails, on purpose. The PR carries the detailed audit: commits, the PR description, per-pass review reports, feedback threads, CI status. Linear gets one consolidated message per run: the run summary (outcome, what ran, autonomous decisions made, follow-ups) or the parked note. No comment-by-comment progress spam in the tracker. The detailed record stays where the code is.
Test it before you trust it
Two stages, cheap to expensive:
- Laptop simulation, no Cyrus. Run
claude -pwith the Linear MCP unconfigured and a crafted prompt following the template above with a fake ticket. This exercises transport detection, the handoff file, the state-change no-ops, and final-message emission without touching a real ticket or container. - One throwaway ticket, live. The simulation can't verify the one thing it doesn't control: whether the relay delivers the final message to Linear verbatim. Create a disposable issue, label it
FullAuto, let the real instance run end to end, and diff the Linear comment against what the session actually produced, character for character.
Living with it
Day to day it works the way the last post described. Small, well-specified tickets get a FullAuto label during backlog grooming, and they come back merged with a summary, or parked with a reason. The parks have all been legitimate: an ambiguous acceptance criterion, a CI timeout, a reviewer tie that deserved a human call. Everything in the previous post's "when you shouldn't use this" list applies double here, because the container has no one watching it.
ccmagic (including docs/cyrus-deployment.md, the full deployment reference this post is based on) is at github.com/devondragon/ccmagic.

No comments yet