
Write the prose you own; import the guides you share. cc-guides composes AGENTS.md, CLAUDE.md, and shell scripts from local fragments and shared guides pulled in by reference, and its Guides workflow makes CI the only writer: edit a fragment, push, and the artifacts re-render and commit themselves.

Get started
brew install yasyf/tap/cc-guides
A repo describes each generated file with a .claude/fragments/<target>/ directory: a layout.toml that composes local *.fragment.* prose with imports of shared guides, plus the local pieces. Each imported alias names its source in a [sources.<alias>] table. render fetches each import at a pinned commit, writes the target with a version-free GENERATED marker, and records every pin in a lock file:
$ cat .claude/fragments/AGENTS.md/intro.fragment.md
# Acme
House rules for agents.
$ cat .claude/fragments/AGENTS.md/layout.toml
fragments = [
"intro",
"cc-skills:ccx",
]
[sources.cc-skills]
source = "github:yasyf/cc-skills@main"
$ cc-guides render
rendered .claude/fragments/AGENTS.md -> AGENTS.md
$ head -6 AGENTS.md
<!-- GENERATED by cc-guides from .claude/fragments/AGENTS.md/ — do not edit; edit the fragments and push — CI renders. -->
# Acme
House rules for agents.
## Compact Context (ccx)
intro is your prose; cc-skills:ccx arrives verbatim from the shared guides in cc-skills, identical across every repo that imports it. Change the shared guide once and every repo re-renders. render also writes .claude/fragments/cc-guides.lock, which pins every source to the exact commit the artifacts were built against. That first render is the last one anyone runs by hand: wire in the Guides workflow below and CI owns the artifacts — edit a fragment, push, and the render commit follows.
Driving with an agent? Paste this:
Install cc-guides (`brew install yasyf/tap/cc-guides`). Give AGENTS.md a
.claude/fragments/AGENTS.md/ layout: a layout.toml composing my local
*.fragment.md prose with the shared guides I import. Run `cc-guides render`
once (it writes AGENTS.md and .claude/fragments/cc-guides.lock), commit, and
add the Guides shim workflow from the cc-guides README. From then on edit only
the fragments and push — CI re-renders and commits the artifacts; never run
render again. Reference: `cc-guides --help`.
Use cases
Let CI own the rendered artifacts
Rendered artifacts drift when humans and agents write them; the fix is to stop writing them. Every consumer carries the same never-changing shim, and the logic lives in this repo's reusable workflow — it evolves without another consumer edit:
name: Guides
# Stub — all logic lives in yasyf/cc-guides. Edit fragments; CI renders.
on:
push: {branches: [main]}
pull_request:
repository_dispatch: {types: [cc-guides-render]}
schedule: [{cron: "17 9 * * *"}]
workflow_dispatch:
permissions: {contents: write}
jobs:
guides:
uses: yasyf/cc-guides/.github/workflows/guides.yml@main
secrets: inherit
A push to main — and the daily cron, and either manual dispatch — runs ci-render: re-render everything, skip a lock diff that only moved source pins, otherwise commit chore: re-render guides (…) and push. A pull request writes nothing; pr-check fails it the moment it hand-edits a CI-owned file:
$ cc-guides pr-check main
AGENTS.md is CI-owned (rendered by cc-guides) — edit .claude/fragments/AGENTS.md/ instead
$ echo "exit: $?"
exit: 1
A fragment-only PR passes (cc-guides: no CI-owned artifact was hand-edited); the artifacts it implies land in the render commit right after merge. With a CC_GUIDES_DEPLOY_KEY repo secret (a deploy key with write access), the render commit pushes over SSH and still triggers your other workflows; without one, checkout falls back to GITHUB_TOKEN and the commit triggers nothing downstream. Adopting this outside a single-owner fleet? Pin the uses: ref to a release tag or commit SHA instead of @main, and pass the deploy key explicitly instead of secrets: inherit — the shim above trusts this repo's main branch with every secret the job can see.
check stays the local verifier: it re-composes each target in memory — pinned to the commits the lock records — and byte-compares against disk:
$ cc-guides check
OK AGENTS.md
$ echo "agent slop" >> AGENTS.md
$ cc-guides check
STALE AGENTS.md
$ echo "exit: $?"
exit: 1
OK, STALE, and MISSING go to stdout as TSV; exit 1 signals drift, 2 an invalid layout.
Keep one repo's spin on a shared guide
A repo needs its own version of a guide the rest of the fleet shares. Compose a local fragment in the slot where the import would sit — no import, no shadowing:
fragments = [
"intro",
"ccx", # local ccx.fragment.md, not cc-skills:ccx
]
render reads ccx.fragment.md from the artifact dir instead of fetching the shared guide, and a layout that imports nothing locks no sources at all.
Adopt the lock on an older repo
A repo rendered by an older cc-guides (0.1.12 or earlier) carries a per-artifact banner and no .claude/fragments/cc-guides.lock. check and the Guides workflow are lock-driven, so adopt the lock with one render against the current binary — it rewrites each artifact with a version-free marker and writes the lock:
$ cc-guides render
rendered .claude/fragments/AGENTS.md -> AGENTS.md
$ git add .claude/fragments/cc-guides.lock AGENTS.md
Commit the lock alongside the artifacts and add the Guides shim; from then on CI keeps both current.
Composition
An artifact dir is any directory under .claude/fragments/ that holds a layout.toml, and its path below that root is the target it renders. .claude/fragments/AGENTS.md/ renders AGENTS.md; a nested path renders a nested target. An optional target key names the artifact instead, which frees a repo from carrying a directory that shadows a file its tools read. .claude/fragments/gitignore/ with target = ".gitignore" renders the root .gitignore, and the tree holds only the fragment dir:
target = ".gitignore"
fragments = [
"cc-skills:gitignore-base",
"gitignore-local",
]
Renaming an existing artifact dir means adding the target key in the same move, and a full render enforces that. Renaming .claude/fragments/.mcp.json/ to .claude/fragments/mcp.json/ without the key would render mcp.json and orphan the real .mcp.json; the render refuses, because a locked artifact whose file is still on disk may not lose its dir. Removing an artifact for good is the same gesture minus the rename — delete its dir and its rendered file together, or keep the file and pass --prune.
The guards hold either way: a target stays inside the repo, carries a supported extension, and never lands back under .claude/fragments/. A target two artifact dirs share, an override colliding with another dir's path included, is refused in the preflight before anything is written. The generated marker and pr-check both name the dir that rendered the file, which under an override differs from the target's own path. The kind — Markdown, shell comment style, JSON, YAML, TOML, or gitignore — comes from the target extension. A JSON target deep-merges its pieces (arrays concatenate, objects merge) and carries no marker; the lock is its only drift record. A YAML target concatenates its pieces like Markdown (never a semantic merge, so load-bearing comments survive), carries a #-comment marker, and re-validates the composed document at render. A TOML target concatenates the same way — fragments are disjoint table sets, so comments and the marker survive — with one authoring rule: every fragment after the first must open with a table header, so a stray root-level key can't silently land inside the previous fragment's last table. The composed output re-validates at render, catching a slip like the same table defined in two fragments before it ever reaches a consumer. TOML lint tolerates {{token}} placeholders by neutralizing each distinct token to a distinct numeric literal; a token inside a typed scalar (a date like 2026-{{month}}-01) still neutralizes to a malformed value and gets flagged — context-free substitution cannot satisfy every typed position. A gitignore target concatenates its pattern fragments the same way and carries the same #-comment marker; it has no syntax to validate, so any pattern — a {{token}}-bearing one included — passes lint.
layout.toml is an ordered, heterogeneous fragments array, optionally preceded by a target override. Both are top-level keys and come before any [sources.*] table: a top-level key written after a table header nests inside that table, and the binary hard-errors on that shape instead of composing empty.
fragments = [
"agents-md", # local: agents-md.fragment.md in this dir
"cc-skills:ccx", # import: the ccx guide from cc-skills
{ use = "cc-skills:install-binary-latest", args = { binary = "slop-cop", plugin = "slop-cop", repo = "yasyf/slop-cop", brew = "yasyf/tap/slop-cop" } },
]
[sources.cc-skills] # required: every imported alias declares its source
source = "github:yasyf/cc-skills@main"
A string entry is a local <name>.fragment.<ext> or an alias:name import; an inline table adds args that fill {{token}} placeholders in the imported body. Every imported alias needs its own [sources.<alias>] table — there is no baked-in default. A source spec takes one of two forms: the manifest form github:<owner>/<repo>[@<ref>], which follows the target repo's cc-guides.toml to its guides dir, or the explicit-path form github:<owner>/<repo>//<path>[@<ref>], which points straight at a subtree. Pieces join with one blank line between them, LF only, one trailing newline. Prose is never token-substituted, so ${{ github.sha }} and {{VAR}} survive verbatim.
Commands
One invocation per surface; run cc-guides <command> --help for the full flag list.
| Command |
What it does |
render [paths…] |
Compose each artifact dir to its target and write the lock. No paths: discover every layout under the repo. A full render refuses to drop a locked artifact whose file is still on disk; --prune allows it. |
check [paths…] |
Re-compose in memory, pinned to the lock's commits, and byte-compare. TSV OK/STALE/MISSING, plus ORPHANED for a locked target no dir renders; exit 1 on drift, 2 on invalid input. |
ci-render |
Render everything, skip a lock diff that only moved source pins, otherwise commit chore: re-render guides (…) and push, retrying a rejected push. The render job's engine. |
pr-check <base> |
Exit 1 when <base>...HEAD touches a lock-listed artifact or the lock itself, naming the artifact dir that renders it — the real dir, target override included; a repo with no lock owns nothing and passes. |
lint <dir> |
Check a shared-guides directory for purity: LF, one trailing newline, kind, shell shebang. |
list |
List each artifact dir and the fragments it composes. |
cat <ref> |
Print a fragment body: an alias:name import or a local piece by name. |
--source alias=<spec> overrides where an import resolves — a github: spec or a local directory for development. --version prints the cc-guides version, which render stamps into the lock.
How it fits together
cc-guides ships three things: this binary, the reusable Guides workflow every consumer shims onto, and an install action. The content lives in its consumers — cc-skills is the reference home for the shared guides, and each repo carries its own layouts. An import resolves to an immutable commit through git ls-remote, fetches that tree from codeload, and caches it under the user cache dir; every artifact in a run pins the same sha, recorded in .claude/fragments/cc-guides.lock. check reads those pins from the lock, so it reproduces an artifact offline once the cache is warm and never false-fails across binary versions. Build, test, and lint conventions live in AGENTS.md.
task test # go test -race ./...
task ci # vet, lint, test, build
Licensed under MIT.