Pre-seeding agents, skills, MCP servers, and secrets
The bundle sections that ship a fully-configured workspace on first launch.
A bundle's value is what it puts in front of the user on first launch. This page covers the four sections that do the heavy lifting: agents[], userSkills[], userMcpServers[], and the secret_ref URIs that thread credentials through all of them.
Pre-seed agents
The agents[] array scaffolds one or more agents under the configured agents_root on the user's machine. Each agent can include a starting CLAUDE.md, skills, MCP servers, and arbitrary starter files.
{
"agents": [
{
"id": "acme-marketing",
"displayName": "Marketing",
"createMode": "from-template",
"relativePath": "Marketing",
"pinned": true,
"claudeMd": "You are the Marketing agent for Acme. Default tone: warm and direct.\\nReference files in `brand/` before drafting.\\n",
"skills": [
{ "name": "summarize", "source": "bundled" },
{ "name": "acme-press-release", "content": "...inline markdown..." }
],
"mcpServers": [ /* ... */ ],
"files": [
{ "relativePath": "brand/voice-guide.md", "content": "..." }
]
}
]
}| Field | Notes |
|---|---|
id (required) | Stable key for re-apply deduplication. Use <org>-<purpose> — acme-marketing. Never reuse across distinct agents. |
displayName (required) | Renamable by the user; for stable identity, use id. |
createMode | from-template (default, standard scaffold), empty, sample (curated example files). |
relativePath | Folder under agents_root. Defaults to displayName if unset. |
claudeMd | Markdown string written to <agent>/.claude/CLAUDE.md. Use \n for line breaks in JSON. |
pinned | When true, appends to AppConfig.pinned_agents so the agent appears at the top of the roster. |
skills[] | Reference a bundled starter ({ "name": "summarize", "source": "bundled" }) or ship inline with a content field. |
mcpServers[] | Inline MCP server configs (see below). |
files[] | Arbitrary files dropped into the agent folder. Each has a relativePath and content. Reserved paths (.easycc/, .claude/CLAUDE.md, .claude/skills/) are blocked. |
The disk layout each agent gets: .easycc/config.json, .claude/CLAUDE.md, .claude/skills/<skill>/SKILL.md per skill, .claude/mcp-servers.json, plus your custom files.
Common patterns:
- One agent + comprehensive skills — solo founders, single-purpose deployments.
- Multiple agents + role division — a Marketing agent, an Operations agent, a Customer-Support agent for a small business.
- Agent + starter files — drop templates, brand guides, or boilerplate into
files[]so users open EasyCC to a workspace, not a blank folder. - Sample + working agent — one
createMode: sampleagent for onboarding, plus a working agent for real work.
Pre-seed skills
Skills come in two scopes:
| Scope | Where in the bundle | Visible in |
|---|---|---|
| Per-agent | Inside an agent's skills[] array | Only that agent |
| User-scope | Top-level userSkills[] array | Every agent the user has |
Per-agent skills can reference a bundled starter by name + source: "bundled", or ship inline content. User-scope skills are always inline — no bundled-source shortcut.
{
"userSkills": [
{
"name": "acme-house-style",
"content": "---\\ndescription: Apply Acme house style to the current file.\\n---\\n\\n# Acme house style\\n\\nWhen running this skill...",
"approved": true
}
]
}Frontmatter fields supported in skill content: name, description, category, tags, allowed-tools, model. The approved: true field on a bundled skill skips the first-run approval prompt — use carefully; only set for skills you've vetted.
Maintain skills as .md files in source control, then have a build script read each markdown and inject as the content field of the bundle JSON. Hand-editing the JSON with \n escapes gets painful fast.
Re-apply rules: EasyCC hashes the content of each shipped skill. On re-apply with a new bundle, an updated version overwrites only if the user hasn't edited the local copy (their hash still matches the previous shipped hash). User edits are preserved; an "updated" badge appears on the skill row.
Pick per-agent for work-specific skills; pick user-scope for house-style checkers, brand-voice utilities, and other things that apply everywhere. When in doubt, default to user-scope.
Pre-seed MCP servers
Three transports:
| Transport | Fields | Notes |
|---|---|---|
stdio | command, args, env, auth (optional) | EasyCC spawns the subprocess and talks MCP over stdin/stdout. command is either in PATH or absolute. |
http | url (HTTPS), auth | Remote MCP server. |
sse | url, auth | Same as http but server-sent events. |
Three auth shapes:
// Bearer token in keychain
"auth": {
"kind": "bearer",
"secretRef": "keychain:com.easycc.app/mcp.acme-events.token"
}
// Custom headers — each value can be a keychain URI or a literal
"auth": {
"kind": "headers",
"headers": {
"X-Api-Key": "keychain:com.easycc.app/mcp.acme-events.X-Api-Key",
"X-Tenant-Id": "acme-prod"
}
}
// Unauthenticated
"auth": { "kind": "none" }Per-agent MCP servers live in the agent's mcpServers[] and write to <agent>/.claude/mcp-servers.json. User-scope MCP servers live in the top-level userMcpServers[] and write to ~/.claude/mcp-servers.json.
Trust boundary: stdio runs arbitrary executables. Only ship bundles from trusted channels (MDM, signed installer) — anyone who can write bootstrap.json to a discovery location can run any command on the device.
Re-apply: per-agent servers are keyed by (agentId, mcpName) for clean dedup; user-scope servers by name. If the user disabled a server in EasyCC, re-apply respects that — the server stays disabled.
Secrets and secret_ref URIs
All secrets in a bundle are references, never literal values. EasyCC reads the actual value from the OS keychain at apply time. The URI format:
keychain:com.easycc.app/<slot-name>Three places secret_ref appears:
| Location | Field | What it holds |
|---|---|---|
provider | apiKeySecretRef | Claude API key |
mcpServers[].auth (bearer) | secretRef | Bearer token |
mcpServers[].auth.headers.<name> | (per-header value) | Custom header value |
Deployment flow:
- Your MDM pushes the actual secret values to the OS keychain before the bundle applies. See Secrets for the per-MDM scripts.
- The bundle references the slots only.
- On apply, EasyCC reads each
secret_ref, then re-stores under a canonical slot the runtime expects (auth_claude_api_keyfor the provider;mcp.<server-name>.<auth-kind>for MCP). The runtime always reads from canonical slots.
If a referenced secret is missing at apply time, EasyCC fails that section, records the failure in bundle-applied.json, and shows the user a friendly "ask your IT admin" error. The next launch retries automatically — once IT pushes the secret, the section applies cleanly.
Rotation: push the new value to the same slot (your existing MDM script handles this). Old values aren't carried forward inside the bundle — they live in the keychain and are owned by your MDM.
v1 limitation: only keychain: URIs are supported. The runtime returns "your administrator configured a credential type this version doesn't support yet" for anything else. Future versions will add env:, oauth:, provision:, and prompt: schemes.
How is this guide?