Local Project Heroes
Updated June 20, 2026
Local project heroes let you define heroes as files inside a project instead of only in the global Settings roster. Drop a JSON file into .crystl/heroes/ and it becomes a hero for that project, one that can automatically join every quest party, travel with the repo in git, and apply to every isolated worktree.
Use them for project-specific roles you always want present: a security reviewer for an auth-heavy codebase, a docs-keeper, or a house-style enforcer. Defined once, committed alongside the code, and shared with anyone who clones the repo.
Project-local heroes are the committable, project-scoped mechanism. They sit alongside two other sources: the immutable built-in catalog and your global custom heroes (the named copies you get from editing a built-in — see hero shards). On a name clash the most specific wins: project-local > global custom > built-in, so a hero committed here always overrides a global custom of the same name.
Where the files go
Create a .crystl/heroes/ folder in your project and add one JSON file per hero:
my-project/
└── .crystl/
└── heroes/
├── security.json
└── docs.json
You can also use a single .crystl/heroes.json file containing an array of heroes; both forms are loaded and combined.
Manage them in the app (no JSON needed)
You don’t have to hand-write the files or dig through Finder anymore. The Heroes settings page has a Local column that manages everything for you, and writes the same .crystl/heroes/<slug>.json files under the hood.
The Local column groups your heroes by project. Each open project that has local heroes gets its own subheader showing the project’s icon and name, so you can see local heroes across several projects at a glance.
To add a local hero:
- Local column ”+ new” opens crystl’s in-app directory picker. Pick any project folder, even one that isn’t open right now, then the hero editor opens so you can author the hero. On save it lands in that project’s
.crystl/heroes/<slug>.json. - A project subheader’s ”+ new” skips the picker and goes straight to the editor for that project.
To change one, click its edit action. The editor opens with the hero’s current fields, and saving overwrites the same file. No hunting for the path, no reformatting JSON by hand.
The format on disk is unchanged: still the committable JSON described below, still tracked in git, and these heroes still follow the project into every worktree. The editor just saves you the round trip through a text editor. Local heroes can also carry a charter field now, the same standing-orders concept covered in Quest Heroes.
Hero file format
Only instructions is really needed. Everything else is optional.
{
"role_name": "Security",
"guardrail": true,
"hero": "warlock",
"instructions": "Review every change for injection, authz gaps, and secret leakage before it lands. You have veto authority on anything touching auth or shell execution.",
"personality": "Terse, suspicious, allergic to 'we'll fix it later'.",
"stats": { "strength": 5, "intelligence": 9, "wisdom": 9, "dexterity": 6 },
"model": "claude-opus-4-8",
"agent": "claude"
}
| Field | Meaning | Default when omitted |
|---|---|---|
role_name | The hero’s label in the party | the filename (security.json → security) |
guardrail | true = auto-joins every party for this project | false |
hero | Preferred crystal/hero name (e.g. wizard, warlock) for the summoned shard | auto-assigned a free name |
instructions | The hero’s brief: expertise and scope | empty |
personality | Flavor text for the agent’s voice | empty |
stats | strength / intelligence / wisdom / dexterity, each 1 to 10 | 5 each |
model | Model id for this hero | Sonnet |
agent | claude, codex, or antigravity | claude |
The stats, personality, and model fields work exactly like Quest Heroes: local heroes are regular heroes, just defined in your project tree.
Guardrail heroes vs. optional heroes
A hero file is one of two kinds, set by the guardrail flag:
- Guardrail (
"guardrail": true): automatically appended to every party when you start a quest in this project. You don’t pick it; it’s always there. Ideal for an always-on role like a security reviewer or a docs-keeper. - Optional (
"guardrail": false, or omitted): offered in a picker so you can add it to a quest when you want it, but never forced in.
If a guardrail’s role_name already exists in the chosen party, it isn’t duplicated.
How they appear when you start a quest
Open the project and click crystl quest to open the quest setup panel. In the party preview:
- Guardrail heroes show as a pinned role row marked with a shield glyph. They have no edit pencil; the file in
.crystl/heroes/is the source of truth, not the in-app editor. Switch parties in the dropdown and the guardrail stays put. - Optional local heroes appear in a Local Heroes section with a + to add one to the active party (and a × to remove it once added).
Press begin quest and each guardrail (plus any optional heroes you added) is summoned as its own shard alongside the party. A hero with an explicit hero field takes that name when it’s free; otherwise it’s assigned a unique one automatically.
They version in git
The first time you start a quest with local heroes present, crystl adds a carve-out to the project’s .crystl/.gitignore so the heroes folder is tracked while the rest of .crystl/ stays ignored:
*
!.gitignore
!heroes/
!heroes/**
!heroes.json
This means:
- Your heroes are committed alongside your code, so changes show up in diffs and pull-request review and track over time.
- Anyone who clones the repo gets the same heroes.
- Isolated shards (git worktrees) get the same heroes automatically: because the files are committed, they ride into every worktree checkout. No per-worktree setup.
If you’ve customized .crystl/.gitignore yourself, crystl leaves it alone rather than overwriting your rules.
Example
A project that always wants a security guardrail and offers an optional docs hero:
// .crystl/heroes/security.json
{
"role_name": "Security",
"guardrail": true,
"hero": "warlock",
"instructions": "Veto anything touching auth, shell execution, or network calls. Flag risky diffs to the party rather than fixing silently.",
"stats": { "intelligence": 9, "wisdom": 9 },
"model": "claude-opus-4-8"
}
// .crystl/heroes/docs.json
{
"role_name": "Docs",
"instructions": "Keep README and CHANGELOG current as the code changes. Write for a newcomer; prefer short examples over prose."
}
Start a quest in that project and Security joins every party with a shield; Docs waits in the Local Heroes picker until you add it.
Shards and party size
Defining, reading, and summoning local heroes is free; there’s no gate on the files themselves. Every hero in a summoned party counts as a shard, including guardrail heroes and any optional heroes you add from the Local Heroes picker. Shards aren’t capped on any tier, so a large party or a stack of guardrails summons freely, you’re limited only by what your machine can comfortably run.
Because a guardrail is defined by the project rather than chosen per-quest, it joins every party automatically, worth keeping in mind if you’re adding guardrails to a repo other people use.
Mental model
Drop a JSON file in
.crystl/heroes/→ it’s a hero. Mark it"guardrail": true→ it’s in every party for that project, committed to the repo, and it follows the project into every worktree.