# Triple-C marketplace — design Date: 2026-09-27 Status: approved in conversation (user), section by section; awaiting review of this written spec ## Goal Users add one or more **marketplace** git repos in Settings, browse the agents, skills, commands, hooks and plugins they contain, and install any item either **for all projects** or **for individual projects**. Private repos are supported through named sign-in accounts. Installed items are pinned to a commit and only change when the user accepts an update. A public starter marketplace, `shadowdao/triple-c-marketplace` (local clone at `/workspace/projects/triple-c-marketplace`, its own git repo), is created with one example of each item type. It is also where marketplace items built later will be published. Non-goals (this version): per-marketplace auto-update, a registered Triple-C GitHub/OAuth app, publishing to a marketplace from inside Triple-C, a separate OS window for the marketplace. ## Decisions (user-approved) | Topic | Decision | |---|---| | Repo format | **Hybrid**: Triple-C-managed `agents/`, `skills/`, `commands/`, `hooks/`; `plugins/` is a standard Claude Code marketplace installed with `claude plugin` | | Auth | **Named accounts**, one chosen per marketplace. GitHub sign-in reuses `gh` (host first, else inside a running container); any host accepts a pasted token. No Triple-C OAuth app | | Scope | **Global list + per-project additions + per-project opt-out** of global items; global items reach projects created later | | Updates | **Pinned on install**; "update available" per item; the user reviews a diff and accepts | | Fetching | **On the host**, into an app cache, with `gix`; files copied into containers. Tokens never enter containers | | Where the UI lives | Settings sidebar section + a full-width **Marketplace** main-area tab (like Project Home), not an OS window | ## Current state (verified against `292fc90`) - Settings is `components/settings/SettingsPanel.tsx`, rendered in the sidebar, sections as `AccordionSection`s. Main-area tabs are Zustand-driven (`store/appState.ts` `tabOrder` / `activeTabKey`; keys `home:` and session ids), rendered by `App.tsx`, strip in `layout/MainTabs.tsx`. No router. - Global settings: `models/app_settings.rs` `AppSettings` → `/triple-c/settings.json`. Per project: `models/project.rs` `Project` → `projects.json`. TS mirror in `lib/types.ts`, wrappers in `lib/tauri-commands.ts`. Settings export/import in `models/settings_export.rs`. - Keychain: `storage/secure.rs`; global single-value entries use `read_entry`/`delete_entry` (shared Claude token, gateway keys). Project secrets are restricted to `PROJECT_SECRET_KEYS`. - Container start: `commands/project_commands.rs` `start_project_container` runs `docker::sync_bedrock_credentials` after start (≈:1448) — the pattern the marketplace sync follows. - Exec/upload: `docker/exec.rs` `upload_bytes_to_container(container_id, dest_dir, file_name, data)`, `exec_oneshot_streams_as(container_id, user, cmd, env)`, `create_attached_exec_as(…, tty, user)`. Container user is addressed as `"claude"`. Constant-script + env-data rule: header of `commands/inspect_commands.rs`. - `container/entrypoint.sh` merges `CLAUDE_CODE_SETTINGS_JSON` into `~/.claude/settings.json` (≈:408-447), then runs `claude update` under `flock /tmp/.triple-c-claude-update.lock` (≈:649) and prints `Triple-C container ready.` (≈:654). Nothing marks readiness on disk today. - `commands/inspect_commands.rs` `list_container_capabilities` already inventories agents, skills, commands, hooks and plugins in a container (read-only); `CapabilityTiles.tsx` shows it. - The container image has `gh`, `git`, `jq`. Claude Code 2.1.283 supports `claude plugin marketplace add ` / `update` / `remove` and `claude plugin install|uninstall @`. - No host-side git or GitHub auth exists today (no `git2`/`gix`; `reqwest` with rustls is present). `gix` 0.88 is current on crates.io. ## 1. Marketplace repo format ``` / ├── README.md ├── agents/.md # Claude Code agent file (front matter: name, description) ├── skills//SKILL.md (+ files) # Claude Code skill folder ├── commands/.md # Claude Code slash command ├── hooks//hook.json (+ scripts) # Triple-C hook manifest └── plugins/ ├── .claude-plugin/marketplace.json # standard Claude Code marketplace catalog └── /… # standard Claude Code plugins ``` Every folder is optional; a repo with only `plugins/` is valid. - **Agent** — any `agents/*.md`. Name/description from YAML front matter (`name` falls back to the file stem). Installs to `~/.claude/agents/`. - **Skill** — any `skills//` containing `SKILL.md`; name/description from its front matter. Installs to `~/.claude/skills//`. - **Command** — any `commands/*.md`; description from front matter `description` if present, otherwise the first non-empty line. Installs to `~/.claude/commands/`. - **Hook** — `hooks//hook.json`: ```json { "name": "notify-on-stop", "description": "Desktop ping when Claude finishes", "hooks": { "Stop": [{ "hooks": [{ "type": "command", "command": "${HOOK_DIR}/notify.sh" }] }] } } ``` `hooks` is Claude Code's `settings.json` hooks object, verbatim. `${HOOK_DIR}` is substituted with the install folder `~/.claude/triple-c/hooks/` (absolute path). The whole folder is copied; files keep their executable bit from the git tree mode. - **Plugin** — entries of `plugins/.claude-plugin/marketplace.json`; each `source` must be a relative path inside `plugins/` (remote sources are listed as invalid: they would fetch from inside the container, bypassing pinning and host-side auth). Item identity: `(marketplace_id, kind, key)` where `key` is the file stem / folder name / plugin name. Keys must match `^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$`. Invalid items (bad front matter, unparsable `hook.json`, unknown hook event, key failing the pattern, plugin source missing or outside `plugins/`, symlinks anywhere in an item) are listed with the reason and cannot be installed; they never stop the rest of the marketplace from loading. Limits (defensive, per item): 2 MiB total, 200 files. ## 2. Data model and storage `AppSettings` (all `#[serde(default)]`): ```rust pub marketplace_accounts: Vec, pub marketplaces: Vec, pub global_marketplace_installs: Vec, pub struct MarketplaceAccount { pub id: String, // uuid pub label: String, // user-facing, e.g. "Work GitHub" pub host: String, // e.g. "github.com", "repo.anhonesthost.net" pub method: AccountMethod, // GhHost | GhContainer | Token pub username: Option, // resolved at sign-in, display only } pub struct Marketplace { pub id: String, // uuid pub name: String, // display; slug used in container paths pub url: String, // https URL only pub branch: Option,// None = remote default branch pub account_id: Option, // None = anonymous } pub struct MarketplaceInstall { pub marketplace_id: String, pub kind: ItemKind, // Agent | Skill | Command | Hook | Plugin pub key: String, pub commit: String, // full hex object id } ``` `Project` (all `#[serde(default)]`): ```rust pub marketplace_installs: Vec, // project-only additions pub marketplace_disabled: Vec, // (marketplace_id, kind, key) opted out ``` **Secrets.** Token and GhContainer accounts keep their token in the keychain as `marketplace-account:` (new global helpers in `secure.rs` alongside the gateway ones). GhHost accounts store nothing: every fetch runs `gh auth token --hostname ` so a later `gh auth refresh`/logout on the host is honoured. Deleting an account deletes its entry. **Effective set for a project** (pure function, unit-tested): `(global − project.marketplace_disabled) ∪ project.marketplace_installs`, keyed by `(marketplace_id, kind, key)`; on a key clash the project's entry (and pin) wins. **Cache.** `/triple-c/marketplaces/.git` — a bare `gix` clone. Content is read from git objects at each install's pinned commit, never from a worktree, so different pins of the same repo coexist. Pinned commits are protected from pruning by writing a ref per pin (`refs/triple-c/pins/`), refreshed after every install/update/remove. Removing a marketplace deletes its cache and its account link; installs that still reference it stay in the lists and are shown as **source removed**. With no cache there is nothing to copy, so the next sync removes those items from containers; the UI says so before the marketplace is removed and offers "Forget" to drop the stale entries. **Export/import.** Accounts (without secrets), marketplaces and install lists go into the existing export; account tokens follow the existing encrypted-secrets policy of `settings_export.rs`. ## 3. Fetching and signing in **Fetch** (`marketplace/git.rs`): `gix` over HTTPS only (reject other URL schemes at add time). Credentials are supplied through gix's credential callback, never written to disk: username `x-access-token` for github.com, otherwise the account's `username` (falling back to `oauth2`), password = token. Shallow fetch is not used (pins need history for diff/ancestry). - **Add marketplace** = test fetch of the chosen branch; failures surface immediately. - **Refresh**: when the Marketplace tab opens and the last fetch is > 15 min old, on the Refresh button, and once at app start (background, errors logged not toasted). - **Offline / fetch error**: the last cache stays usable; UI shows "last fetched