Materializer pattern
A Materializer transforms a logical artifact (agent, hook, skill) into files on disk inside a project folder. The pattern is codified in ADR 0003: the core never writes these artifacts directly; it goes through a dedicated port.
Why a dedicated pattern
Section titled “Why a dedicated pattern”Without a Materializer, the core would write .claude/agents/*.md, .claude/hooks/*.json, etc. directly. That would create several problems:
- Coupling: the core would know the agent file format (Markdown YAML, JSON, etc.) — that’s not its job.
- Boundaries: impossible to mock writes for tests without patching
fs. - Evolution: changing the format (e.g. adding a signature) would require touching the core.
The Materializer isolates all that behind a port:
export interface AgentWorkspaceMaterializer { materialize(input: MaterializeAgentInput): Promise<MaterializedAgent>; unmaterialize(input: { projectPath: string; sourceId: string }): Promise<void>; list(projectPath: string): Promise<readonly MaterializedAgent[]>;}The buildForCatalogue use-case consumes this port. The concrete ClaudeAgentWorkspaceMaterializer implementation knows the Claude Code format.
Shipped Materializers
Section titled “Shipped Materializers”| Materializer | Target | Output format |
|---|---|---|
ClaudeAgentWorkspaceMaterializer | .claude/agents/<slug>.md | Markdown + YAML frontmatter |
HookWorkspaceMaterializer | .claude/hooks/<name>.json | JSON |
SkillWorkspaceMaterializer | .claude/skills/<name>/ | Folder + manifest |
All write into the project folder (<projectPath>/.claude/), never elsewhere. The Filesystem allowlist validates paths.
When to use a new Materializer
Section titled “When to use a new Materializer”- You want to expose a logical artifact (e.g. “governance policy”) as a file consumable by an external tool (an editor, an AI agent, a CI).
- The output format can evolve without affecting the core.
- Several targets may exist (e.g. an agent for Claude Code + an agent for Codex CLI → two Materializers).
Avoid the Materializer pattern for purely internal writes (chat sessions, memory) — those go through their dedicated store.
Materialize / unmaterialize cycle
Section titled “Materialize / unmaterialize cycle”Install agent (forCatalogue.install use-case) ↓[Read profile from Cortex] ↓[Materializer.materialize → write .claude/agents/<slug>.md] ↓[Update <project>/.arka-deck/agents/installed.json] ↓EventBus → 'agent.installed' (async)
Remove agent (forCatalogue.remove) ↓[Materializer.unmaterialize → delete .claude/agents/<slug>.md] ↓[Update installed.json] ↓EventBus → 'agent.removed' (async)The installed.json tracking allows safe purge: only tracked files are deleted, never user artifacts (other non-arka-deck agents in .claude/agents/).
Testing a Materializer
Section titled “Testing a Materializer”The concrete implementation is testable like any adapter. For consuming use-cases, you inject a FakeAgentWorkspaceMaterializer:
const fakeMaterializer: AgentWorkspaceMaterializer = { async materialize(input) { return { /* ... */ }; }, async unmaterialize() {}, async list() { return []; },};
const forCatalogue = buildForCatalogue({ catalogueClient, agentMaterializer: fakeMaterializer, // ...});See also
Section titled “See also”- ADR: ../../adr/0003-materializer-pattern.md
- Overview: ./overview
- Outbound ports: ./ports-outbound