# Authoring profiles
A profile is a small JSON file that tells `claude-profile` exactly which plugins, skills,
marketplaces, and MCP servers should be active for a session. Each is vendored (cloned/copied)
into that profile's own private directory and loaded for the session via `--plugin-dir`;
nothing else on the machine is registered at all.
> **In a hurry?** `claude-profile new <name>` scaffolds an empty profile in
> `~/.claude-profiles/` with every field present and ready to fill in — see
> [`new`](commands.md#new).
To find candidate plugins to add, use [`find`](commands.md#find).
## Where profiles live
`claude-profile <name>` resolves `<name>.json` by searching, in order (first match wins):
1. `$CLAUDE_PROFILE_DIR/<name>.json`, if the env var is set.
2. `./profiles/<name>.json` (project-local).
3. `./.claude-profiles/<name>.json` (project-local).
4. `~/.claude-profiles/<name>.json` (personal profiles).
5. `~/.claude-profiles/packs/<owner--repo>/profiles/<name>.json`, one candidate per installed
pack (see [`install`](commands.md#install)).
6. `~/.claude-profiles/bundled/<name>.json`: the reference profiles shipped with
`claude-profile` itself. See [Bundled profiles](#bundled-profiles). They are baked into the
binary and written to this directory on the first run after install or upgrade.
`~/.claude-profiles/bundled/` belongs to the engine: every upgrade rewrites it and deletes
profiles that are no longer shipped. To customize a bundled profile, copy it up one level
into `~/.claude-profiles/`, where step 4 takes precedence over step 6.
`claude-profile list` shows every profile it can find across all of these locations and where
each one came from.
## A worked example
A minimal profile is just a name and one plugin:
```json
{
"name": "rust-minimal",
"description": "TDD-driven Rust development",
"marketplaces": {
"superpowers-marketplace": "obra/superpowers-marketplace"
},
"plugins": [
"superpowers@superpowers-marketplace"
],
"pluginDirs": [],
"mcpServers": {}
}
```
Launching it (`claude-profile rust-minimal`) clones the `superpowers-marketplace` marketplace
if it isn't already cached, vendors a copy of the `superpowers` plugin into
`~/.claude-profiles/store/rust-minimal/vendor/` if it isn't already there, then launches
`claude --plugin-dir`-ed at only that vendor directory. Nothing else on the machine is loaded
into the session.
## Bundled profiles
`claude-profile` ships these profiles. Run `claude-profile list` to see them, or
`claude-profile show <name>` to inspect one with `extends` expanded.
| `clean` | none | Plain Claude Code: no plugins, skills, or MCP servers. |
| `dev-base` | none | Shared base: the spec-driven `openpowers`/`superpowers` workflow, live docs (`context7`), code review, commit plugins, and the `intent` and `impeccable` design plugins. Needs the `openspec` CLI, which you install separately (`npm i -g @fission-ai/openspec`). |
| `rust-developer` | `dev-base` | Rust, with rust-analyzer. |
| `rust-desktop-developer` | `dev-base` | Tauri desktop apps. |
| `python-developer` | `dev-base` | Python backends. |
| `ruby-developer` | `dev-base` | Ruby, with ruby-lsp. |
| `rails-developer` | `dev-base` | Ruby on Rails. |
| `rails-hyperdrive` | none | MCP-only add-on for live introspection of a running Rails app. Launch it alongside `rails-developer`. |
| `go-developer` | none | Go, with gopls. |
| `java-developer` | none | Java, with jdtls and Spring Boot. |
| `dotnet-developer` | none | .NET and C#. |
| `typescript-developer` | none | TypeScript and Node. |
| `react-developer` | none | React and Next.js. |
| `vue-developer` | none | Vue and Nuxt. |
| `angular-developer` | none | Angular. |
| `frontend` | none | Turning HTML designs and a design guide into working UI. |
The stack profiles add a language server and backend, database, performance, testing, review,
and commit plugins. They were composed with [`find`](commands.md#find) against the
cross-marketplace plugin index. See [Inheriting from another profile](#inheriting-from-another-profile)
for how `extends` works, using `rust-developer` and `dev-base` as an example.
To combine profiles, list them in one launch:
```sh
claude-profile rails-developer rails-hyperdrive
```
## Fields
| `name` | string, required | Profile name. Should match the filename (without `.json`). |
| `description` | string, optional | Human-readable summary shown by tooling. |
| `author` | string, optional | Profile author, shown by [`show`](commands.md#show). When absent, `show` falls back to the source repo owner (for pack/URL profiles), else `—`. |
| `marketplaces` | object, optional | Maps a local marketplace name to its source: `owner/repo` (floating, tracks the default branch), `owner/repo#ref` (pinned to a tag/branch/SHA, checked out at that ref and never silently moved), or a full `https://…` / `git@…` SSH git URL (also `#ref`-capable). |
| `plugins` | array of strings, optional | Plugins/skills to vendor, as `plugin@marketplace` ids (or `<name>@skills-dir` for a personal loose skill). Each is copied into this profile's own `~/.claude-profiles/store/<profile>/vendor/`; nothing is installed globally. |
| `pluginDirs` | array of strings, optional | Local paths passed to `claude --plugin-dir`, for plugins developed alongside the profile rather than published to a marketplace. |
| `mcpServers` | object, optional | MCP server definitions passed via `--mcp-config`. Because launches use `--strict-mcp-config`, **any MCP server bundled inside a vendored plugin is otherwise dropped**; redeclare it here if you need it. Empty (`{}`) means no MCP servers for the session. |
| `bare` | boolean, optional (default `false`) | API-key-only absolute isolation. Passes `--bare` to `claude`, which additionally suppresses global/project `CLAUDE.md` and auto-memory: see [how it works](how-it-works.md#what-is-not-gated). Requires `ANTHROPIC_API_KEY` authentication; it drops OAuth/keychain login. |
| `removePlugins` | array of strings, optional | Plugins (as `plugin@marketplace` ids) to drop from the set inherited when this profile `extends` another. Has no effect unless the profile sets `extends`. See [Inheriting from another profile](#inheriting-from-another-profile). |
## Version pinning and the lockfile
A `marketplaces` entry can float (`owner/repo`, tracks the default branch) or pin
(`owner/repo#ref`, checked out at a specific tag/branch/SHA).
The first time a profile is provisioned, `claude-profile` writes a `<profile>.lock` JSON file
(sibling to the profile for personal/project profiles; under `~/.claude-profiles/locks/` for
pack and example profiles) recording the resolved commit SHA of every marketplace it
installed. On every subsequent launch, even for floating marketplaces, `claude-profile`
provisions to the **locked SHA**, not whatever the branch currently points to. That is the
reproducibility guarantee: the same profile resolves to the same plugin code across machines
and over time, until you explicitly move it.
`claude-profile update profiles` git-pulls installed profile repos (packs) and, for every
discoverable profile, re-resolves each **floating** (unpinned) marketplace to its current HEAD
commit and rewrites that profile's `.lock` with the new SHA. Marketplaces pinned to an explicit
`#ref` in the profile JSON are never moved by `update profiles`.
`update profiles --frozen` moves nothing: it pulls no packs and re-resolves no pins. Instead it
checks whether any discoverable profile's lock is stale (references a marketplace that's missing
from its `.lock`) and **fails** (non-zero exit) if so, naming every stale profile. Use `--frozen`
in CI/scripts to assert "nothing needs updating" without mutating anything. See
[`update profiles`](commands.md#update-profiles) for the full flag reference.
(Plain `claude-profile update`, with no subcommand, checks for a newer `claude-profile` release
instead — it does not touch profiles or packs. See [`update`](commands.md#update).)
## Inheriting from another profile
A profile can set `"extends": "some-base-profile"` to build on another profile instead of
repeating its contents. `claude-profile` looks up the named base through the same search path
it uses for any profile, then merges the two:
- **`plugins`**: the base's plugins first, then the child's, with duplicates removed. Any id
listed in the child's `removePlugins` is then dropped from the result.
- **`marketplaces`** and **`mcpServers`**: the base's entries, overlaid with the child's. If
both define the same key, the child's value wins.
- **`pluginDirs`**: the base's, then the child's, with duplicates removed.
- **`description`**: the child's if set, otherwise the base's.
- **`bare`**: true if either profile sets it.
Inheritance is one level deep. If the base profile itself sets `extends`, or a profile extends
itself, `claude-profile` reports an error instead of following the chain.
For example, a team base profile with the shared toolchain, and a personal profile that adds
one plugin and drops another:
```json
{
"name": "my-rust",
"extends": "team-rust",
"plugins": ["my-scratch-plugin@my-marketplace"],
"removePlugins": ["pair-programming@team-marketplace"]
}
```
## The plugin I want isn't in a marketplace
Every id in `plugins` is resolved through some marketplace's `marketplace.json`;
`claude-profile` has no way to vendor a bare `owner/repo` that only contains a
`plugin.json`. Pointing a `marketplaces` entry at such a repo fails — there is no
manifest to read. When you have a plugin but no marketplace, pick one of these:
1. **It probably *is* in a marketplace — find it.** Many standalone plugin repos are
also published (via a `url` or `git-subdir` source) inside a larger marketplace.
Run [`find`](commands.md#find) to search the cross-marketplace index; if it turns
up, reference it as `plugin@that-marketplace` and declare *that* marketplace, not
the plugin's own repo. For example, `obra/the-elements-of-style` is a plugin-only
repo, but the plugin ships through `superpowers-marketplace`, so you write
`elements-of-style@superpowers-marketplace` and declare
`"superpowers-marketplace": "obra/superpowers-marketplace"`.
2. **Point `pluginDirs` at a local clone.** Clone the plugin repo yourself and add its
path to `pluginDirs`. That path goes straight to `claude --plugin-dir`, bypassing
marketplaces entirely — the fit for a plugin you're developing, or one that only
exists as a repo. Note that `pluginDirs` entries are filesystem paths, not `@`-ids,
and are not vendored or SHA-locked the way marketplace plugins are.
3. **If it's really just skill(s), use `@skills-dir`.** Drop the skill folder under
`~/.claude/skills/<name>` (or project-local `.claude/skills/<name>`) and reference
it as `<name>@skills-dir`. See [Referencing a personal loose skill](#referencing-a-personal-loose-skill).
## Referencing a personal loose skill
A **loose skill** living directly under `~/.claude/skills/<name>` or
`.claude/skills/<name>` (project-local takes precedence) can be referenced
in a profile's `plugins` list as `<name>@skills-dir`. claude-profile vendors
a *copy* of that folder into the profile's own vendor directory, generating
a minimal `.claude-plugin/plugin.json` manifest on the copy if the original
doesn't have one. The original skill folder is never modified — only the
vendored copy gains a manifest.