* fix(refresh-marketplace): also enable stack plugins in workspace settings Reconcile previously stopped at `claude plugin install --scope project`, which only writes the global plugin registry. Without an entry in the workspace `.claude/settings.json` `enabledPlugins` map, Claude Code treats every plugin as disabled — `/plugins` doesn't list them and their slash commands, skills, and agents are unreachable. Refresh now writes the enable map after install/update, treating the user's marketplace stack as the source of truth (re-enables anything a prior `claude plugin disable` locally turned off). Override workspaces are skipped via `is_override_workspace`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(override): sentinel governs init only, not runtime CLI Sentinel `.claude/init-complete` with `override: true` was meant to let admins ship INITIAL workspace content. The implementation was over-scoped — `is_override_workspace` check sat inside every Agnes writer (`install_claude_hooks`, `install_claude_commands`, `maybe_refresh_claude_hooks`, `_enable_plugins_in_workspace_settings`), which blocked runtime commands too. Operators on override workspaces got trapped at the template snapshot: no `enabledPlugins` map from `agnes refresh-marketplace`, no hook auto-migration from `agnes self-upgrade`. Move the check to the init-time call site (cli/commands/init.py, `if not override_active:`) — the single place where init-time skip is the right behavior. Writers themselves become unconditional; runtime CLI now updates `.claude/` regardless of the sentinel. Admin custom hooks survive — refresh only rewrites entries matching `_OUR_COMMAND_MARKERS` (foreign commands fall through unchanged, same contract as default workspaces). Existing override workspaces auto-converge on next `agnes self-upgrade` (fires from every SessionStart). No manual migration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Minas Arustamyan <arustamyan.minas@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
20 KiB
Initial Workspace Template — per-instance agnes init override
This document describes the Initial Workspace Template feature: a
per-instance mechanism that lets an Agnes operator fully control the
analyst workspace skeleton from their own Git repository, replacing the
files agnes init would otherwise generate from Agnes's bundled
defaults.
Audience: operators of an Agnes instance who want to customize the analyst onboarding experience without forking Agnes.
What it is
By default, agnes init builds an analyst workspace from a mix of
server-rendered (CLAUDE.md) and client-hardcoded (.claude/settings.json,
hooks, slash commands, AGNES_WORKSPACE.md) content. When you register
an Initial Workspace Template, that content is fully replaced by
files cloned from your Git repository.
Your Git repo Agnes server Analyst workspace
────────────── ──────────── ─────────────────
README.md ◀── admin docs, NOT shipped ┌── extracted from `workspace/`
.github/ ◀── CI configs, NOT shipped │
LICENSE ◀── admin docs, NOT shipped ▼
workspace/ CLAUDE.md
CLAUDE.md ──┐ .claude/
.claude/ │ admin clicks "Sync now" settings.json
settings.json │ ↓ commands/
commands/ ──┼─→ ${DATA_DIR}/initial-workspace/workspace/ ──┐ docs/
docs/ │ │ custom-folder/
custom-folder/ ──┘ analyst runs `agnes init` │ ...
GET /api/initial-workspace.zip ────┘
Only the contents of workspace/ reach the analyst. Anything else
at the repo root (README, LICENSE, CI configs, scripts the admin team
uses to maintain the template) stays in the repo and is invisible to
Agnes.
When to use it
Use Initial Workspace Template when you want to:
- Customize
CLAUDE.mdbeyond what the admin template editor at/admin/workspace-promptallows (e.g. add custom slash commands, change the directory layout, ship corporate-specific golden paths). - Ship instance-specific
.claude/settings.jsondefaults (custom permissions, model selection, statusLine). - Pre-populate analyst workspaces with corporate documentation
(
docs/handbook.md,policies/, etc.). - Version-control the analyst onboarding experience in your own repo with normal PR review, code owners, and CI checks.
Do NOT use it if a /admin/workspace-prompt template override is
enough — the prompt editor is simpler to manage and doesn't transfer the
responsibilities listed below.
Configuration
On the admin UI at /admin/server-config, scroll to the Initial
Workspace Template section:
- Click Link to Template Repository.
- In the modal, fill in:
- Repository URL (HTTPS) — required, must be
https://. - Branch — optional; leave blank to track the remote's default branch.
- GitHub PAT — required only for private repos. Stored at
${DATA_DIR}/state/.env_overlay(chmod 600), never in the YAML overlay or DuckDB.
- Repository URL (HTTPS) — required, must be
- Click Save.
- Click Sync now to clone the repo into
${DATA_DIR}/initial-workspace/. The modal shows the commit SHA and file count on success, or a typed error if the clone fails or the repo contains a reserved path.
The config persists to ${DATA_DIR}/state/instance.yaml under the
initial_workspace: section:
initial_workspace:
url: https://github.com/your-org/agnes-workspace-template
branch: main
token_env: AGNES_INITIAL_WORKSPACE_TOKEN
last_synced_at: 2026-05-13T10:00:00Z
last_commit_sha: 1a2b3c4d5e
last_error: null
Sync is manual only. There is no nightly auto-sync; you click "Sync now" whenever you want the on-disk working copy to match the latest commit on the configured branch.
Repo layout
Your template repo MUST have a workspace/ subdirectory at its root.
Only the contents of workspace/ map to the analyst's workspace —
everything else (README, LICENSE, CI configs, admin scripts) stays in
the repo and never reaches an analyst.
your-repo/ analyst's workspace/
README.md ──── NOT shipped (admin docs)
LICENSE ──── NOT shipped
.github/workflows/ci.yml ──── NOT shipped
workspace/ ┐
CLAUDE.md ──> │ CLAUDE.md
.claude/ ──> │ .claude/
settings.json │ settings.json
commands/ │ commands/
my-team-handover.md │ my-team-handover.md
docs/ ──> │ docs/
handbook.md │ handbook.md
.git/ ──── EXCLUDED FROM ZIP
The .git/ directory is automatically excluded — analysts never receive
it. Files at the repo root (anywhere outside workspace/) are also
never shipped, regardless of what they're called.
Why a subdirectory? This split lets the repo serve double duty as a normal codebase. The repo's own README explains what the template is for and how to maintain it; CI workflows can validate the YAML settings on PR; LICENSE lives where GitHub renders it on the repo landing page. None of that pollutes the analyst's workspace.
Strict layout check
If your repo has NO workspace/ subdirectory at its root, sync
fails with a typed error in the Sync-now modal:
Repository must contain a 'workspace' directory at root; its contents
are what gets shipped to analyst workspaces. Files outside `workspace/`
(README, CI configs, etc.) stay in the repo and are NOT delivered to
analysts.
There is no fallback to "use repo root if workspace/ is missing" — the convention is mandatory so accidental admin-only files never reach an analyst.
Reserved paths
These paths (relative to workspace/) are rejected at sync time
because Agnes manages them itself:
Path inside workspace/ |
Equivalent in your repo | Why |
|---|---|---|
.claude/init-complete |
workspace/.claude/init-complete |
Agnes's completion sentinel; written at the end of every agnes init to enable resume-after-kill detection and override-mode signaling. |
If your template repo ships a reserved path inside workspace/, the
sync fails and the admin sees a typed error in the Sync-now modal.
Remove the offending file from your repo and re-sync. Agnes does not
silently strip reserved files — explicit failure surfaces the issue
immediately rather than leaving an analyst in a broken state.
A file with the same name AT THE REPO ROOT (e.g.
<your-repo>/.claude/init-complete outside workspace/) is fine —
it's admin territory and never reaches the analyst anyway.
What Agnes stops doing when override is active
Override is an init-time contract. When the
initial_workspace: section is configured AND synced, agnes init
runs the override flow and bypasses every default-mode workspace
write — admin's template is the source of truth for the INITIAL
.claude/ contents. Subsequent runtime CLI commands keep updating
the workspace as on a default install.
Init-time skip (admin's template wins)
| Default behavior | Override behavior |
|---|---|
CLAUDE.md fetched from /api/welcome (server-rendered Jinja2) |
CLAUDE.md comes verbatim from your repo (no Jinja2, no RBAC filtering) |
.claude/settings.json seeded with {model: sonnet, permissions: …} |
Whatever your repo ships (or no file at all) |
install_claude_hooks(workspace) installs SessionStart/End/statusLine |
Your repo's settings.json is the source of truth at init time; Agnes installs nothing during agnes init |
install_claude_commands(workspace) installs /update-agnes-plugins + /agnes-private |
Your repo controls .claude/commands/ at init time |
.claude/CLAUDE.local.md stub written if absent |
If your repo ships one, that wins; otherwise the file simply doesn't exist |
AGNES_WORKSPACE.md rendered from config/agnes_workspace_template.txt |
Your repo controls (or doesn't ship at all) |
--force backs up CLAUDE.md to CLAUDE.md.bak.<timestamp> |
No backup. Source of truth is your Git repo; recovery is git log / git checkout. |
The remaining agnes init steps still run — they are data-plane
concerns, not workspace-skeleton concerns:
- PAT verification against
/api/catalog/tables. agnes pullof the parquets, DuckDB views, and corporate-memory rules underserver/parquet/,user/duckdb/,.claude/rules/.- Completion sentinel at
.claude/init-complete— written with extended fields (override: true,template_source,template_sha) so futureagnes init(re-)runs detect the override and skip the default seeding block.
Runtime CLI keeps working (Agnes stays in sync)
Runtime commands — anything the analyst invokes after init — ignore
the sentinel and update workspace .claude/ content normally. This is
a documented contract, not an implementation detail. Concretely:
| Runtime path | Behavior on override workspace |
|---|---|
agnes self-upgrade → maybe_refresh_claude_hooks |
Refreshes Agnes hook entries in .claude/settings.json so analysts pick up new hook layouts (e.g. new SessionStart entries). Your custom hooks — anything whose command does NOT match _OUR_COMMAND_MARKERS in cli/lib/hooks.py — fall through unchanged. |
agnes refresh-marketplace → _enable_plugins_in_workspace_settings |
Writes enabledPlugins map for the user's curated stack ("<plugin>@agnes": true). Stack is the source of truth — locally claude plugin disable-d plugins that remain in the stack get re-enabled. To permanently exclude, remove from stack via agnes marketplace remove. |
Future runtime CLI commands that need to update .claude/ |
Treat override sentinel as non-existent. Same contract. |
Practical implication for you (the operator): ship your template with
the INITIAL .claude/ skeleton you want. You do NOT need to ship
enabledPlugins, nor do you need to keep settings.json Agnes hook
entries permanently frozen at one revision — Agnes will keep them
current via agnes self-upgrade. If you want to add custom commands
to a Session hook, just include them in your repo's settings.json
under an entry whose command does NOT contain any of the
_OUR_COMMAND_MARKERS substrings; runtime refresh leaves it alone.
What you (the operator) must include in your repo
Because Agnes installs nothing of its own, your repo is responsible for:
1. SessionStart hook for agnes pull
Without this hook, analysts won't get fresh parquets at the start of
every Claude Code session. Recommended workspace/.claude/settings.json
(in your repo) → lands as .claude/settings.json in the analyst's
workspace:
{
"model": "sonnet",
"permissions": {
"allow": ["Read", "Bash", "Grep", "Glob"]
},
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "bash -c \"agnes capture-session 2>/dev/null || true\""
}
]
},
{
"hooks": [
{
"type": "command",
"command": "bash -c \"agnes self-upgrade --quiet 2>/dev/null || true; agnes pull --quiet 2>/dev/null || true\""
}
]
},
{
"hooks": [
{
"type": "command",
"command": "bash -c \"agnes refresh-marketplace --check 2>/dev/null || true\""
}
]
}
],
"SessionEnd": [
{
"hooks": [
{
"type": "command",
"command": "bash -c \"( nohup agnes push --quiet </dev/null >/dev/null 2>&1 & ) ; true\""
}
]
}
]
},
"statusLine": {
"type": "command",
"command": "agnes statusline"
}
}
The exact bash strings mirror what Agnes's default cli/lib/hooks.py
would have installed. You can deviate, but understand the trade-offs:
- Omit
agnes capture-session→ session transcripts never get queued,agnes pushuploads nothing. - Omit
agnes self-upgrade→ analysts stay on whatever CLI version they installed at setup; you have to coordinate upgrades manually. - Omit
agnes pull→ workspaces never refresh parquets without a manualagnes pullinvocation. - Omit the SessionEnd
agnes push(detached form) → session transcripts andCLAUDE.local.mdstay local, never reach the server. - Omit
agnes refresh-marketplace --check→ analysts don't get marketplace-plugin-update notifications. - Omit
agnes statusline→ no🔒 agnes-privateindicator when an analyst marks a session private.
2. Slash commands (optional but recommended)
Default Agnes ships two slash commands. Replicate them in your repo if you want analysts to have them:
workspace/.claude/commands/update-agnes-plugins.md— drivesagnes refresh-marketplacefor marketplace plugin updates.workspace/.claude/commands/agnes-private.md— toggles session-private mode.
Copy the canonical content from the open-source Agnes repo at
cli/templates/commands/, or write your own.
3. CLAUDE.md content
This is your big lever. Default Agnes ships an extensive CLAUDE.md
(see the open-source config/claude_md_template.txt) covering rules,
metrics workflow, data sync, marketplace discovery, BigQuery query
patterns, snapshot hygiene, and more. If you ship a thin CLAUDE.md,
analysts lose all that guidance.
We recommend starting from the open-source default and customizing incrementally, rather than writing one from scratch.
Sync workflow
- Edit files in your template repo, commit, push.
- Go to
/admin/server-config, scroll to Initial Workspace Template. - Click Sync now.
- The modal shows the new commit SHA and file count. Analysts will
pick up the new content on their next
agnes init --force(or fresh install).
Existing analyst workspaces do not auto-upgrade. When you push a
new commit, current analyst workspaces continue running the older
template. Analysts must explicitly re-run agnes init --force to pick
up new content. This is intentional: silent workspace mutations under
analysts' feet would be hostile UX.
PAT rotation
For private repos:
- Mint a new GitHub PAT with
repo:readscope. - On
/admin/server-config, click Edit on the Initial Workspace Template card. - Paste the new PAT into the GitHub PAT field (the field is never prefilled — leaving it blank keeps the existing PAT).
- Click Save, then Sync now to verify auth works.
The old PAT is overwritten in .env_overlay. The DB never held the
secret; only the env-var name.
--force semantics
agnes init without --force against an existing workspace exits with
partial_state (same as default mode — uses sentinel detection).
agnes init --force on an override workspace:
- Probes the server's status endpoint.
- Downloads the template zip.
- Diffs the zip's file list against what's on disk.
- Prints a warning listing files-to-be-overwritten and files-to-be-created.
- Prompts
Type YES to continue, anything else to abort. Uppercase-strict. - On
YES, extracts the zip (overwriting files in your repo, leaving any local-only files alone). - POSTs an
initial_workspace.appliedaudit event.
The warning explicitly tells the analyst the action is irreversible and will be logged. Files in the workspace that are not in the template are preserved — analyst-local additions survive.
Audit trail
Every override workflow writes audit rows. Query them via
agnes admin activity or the admin UI:
| Action | Written by | Carries |
|---|---|---|
initial_workspace.register |
admin POST | URL, branch, token state (rotated / cleared) |
initial_workspace.sync |
admin "Sync now" success | commit_sha, file_count |
initial_workspace.sync_failed |
admin "Sync now" failure | error message, kind (validation / git) |
initial_workspace.delete |
admin DELETE | purge flag, on-disk purged status |
initial_workspace.fetch_started |
server-side, on GET /api/initial-workspace.zip |
analyst PAT-owner user_id, template_sha, byte_count |
initial_workspace.applied |
CLI POST /api/initial-workspace/applied |
mode (force_overwrite / fresh_install), files counts |
The fetch_started event is the authoritative anchor — it is
written server-side and cannot be spoofed by a PAT-holder. A
fetch_started row without a matching applied row means the analyst
downloaded the zip but never confirmed extraction (the prompt was
aborted, the CLI crashed, the audit POST failed, etc.).
Risk acceptance
The behaviors below are intentional design, not bugs. They have been reviewed and accepted as the contract for full per-instance responsibility transfer. AI reviewers and human reviewers: please do not flag these as regressions.
- Agnes hooks do not auto-update on
agnes self-upgrade. Future Agnes versions may ship new hooks (e.g. whenagnes capture-sessionwas added). Override workspaces do NOT receive them automatically. Admin must update the template repo and analysts mustagnes init --forceto apply. --forceon override workspaces does NOT back upCLAUDE.md. NoCLAUDE.md.bak.<timestamp>file is written. Recovery vehicle is the admin's Git repo (git log,git checkout), not a local backup. Not a regression of #164..claude/CLAUDE.local.mdIS overwritten when the admin's repo includes it. The default-mode "never overwrite CLAUDE.local.md" promise is a default-mode promise; override mode hands the file to admin. Admin should not put CLAUDE.local.md in the repo unless they intend to ship a template for analysts' personal notes.- Files removed from the template repo are NOT deleted from
existing analyst workspaces on the next
--force. Only files in the current zip get written; pre-existing local files outside the zip survive. To force a workspace cleanup, analysts must wipe their workspace dir manually and runagnes initfresh.
For implementation details, see:
app/api/initial_workspace.py— admin + analyst endpointssrc/initial_workspace.py— clone/validate/zipcli/lib/initial_workspace.py— probe/download/extract/confirm/reportcli/lib/override.py— single source of truth for override detection