Personal Agentic Workspace¶
A tool-agnostic AI collaboration workspace. Organized by function — find what you need by what you want to do.
Commands¶
The root Makefile is the entrypoint. make (or make help) prints
every target. make help-check (part of make check) fails if a target is
missing from that list.
make # catalog
make dashboard # http://127.0.0.1:8787
make workspace NAME="Toy" DOMAIN=personal CLASSIFICATION=low_sensitive
make open-lab ID=tree-1 SCOPE=own
make close-lab ID=tree-1
make voice AUDIO=~/clip.wav
# first time: brew install whisper.cpp && make voice-setup
make code-library # no-op until categories are enabled
make code-library-search Q='tokenizer'
make sandbox IMAGE=alpine
make check # drift gates + tests
Agent skills in shared/skills/ cover the same work plus PKM (AnyType). Prefer
make when you are at a shell.
Getting started¶
Prerequisites: Python 3.11+, uv, git. Optional, only for the capabilities that need them: Calibre + Ollama (library/), Ollama (research/), Node.js/npm (AnyType MCP via npx), the AnyType desktop app (PKM).
- Clone and enable the drift-check hook:
- Configure — review
tool-chest.tomland adjust paths (e.g.calibre_library) for your machine, and set any secrets in your shell profile (never in the file itself). Details: Configuration. - See the catalog, generate client configs, and verify the clone is clean:
- Open the repo in Cursor or Claude Code. Skills and rules load automatically from
.cursor/skills//.cursor/rules/(symlinked fromshared/) — no extra install step. - Set up whichever standalone tools you want, each self-contained via
uv: cd library && uv sync— e-book RAG (needs Calibre + Ollama)cd research && uv sync— Anthropic research RAG (needs Ollama)cd patterns && uv sync— architecture-patterns RAG (needs Ollama)- Browse these docs as a site (optional), via Zensical
(Material for MkDocs' successor — classic mkdocs' maintainer situation has
stalled, and mkdocs-material never shipped support for the interim 2.0 rewrite):
Config:
pip install -r requirements.txt make docs-serve # exports tracked files to docs-build/, then serves at http://127.0.0.1:8000zensical.toml, fully generated — don't hand-edit it. Nav is derived from every tracked markdown file (make docs-config-sync/make docs-config-check, part ofmake check) — add a new doc and it appears on next sync.make docs-buildwrites the static site tosite/. Note: Zensical is alpha software as of this writing.
Capability map¶
| I want to… | Go to |
|---|---|
| See every command | make / make help |
| Capture an idea, task, or person | shared/skills/pkm-capture/ or knowledge/anytype-integration/ |
| Create a project workspace | make workspace NAME=… DOMAIN=… CLASSIFICATION=… |
| Open/close a code analysis lab | make open-lab ID=… SCOPE=… · make close-lab ID=… |
| Open the local dashboard | make dashboard → http://127.0.0.1:8787 |
| Search cloned OSS (enabled categories) | make code-library-search Q='…' |
| Search architecture patterns | knowledge/patterns/ + patterns/ MCP |
| Transcribe a voice note | make voice-setup once, then make voice AUDIO=/path/to.wav |
| Plan a container sandbox run | make sandbox IMAGE=… |
| Pick an agent harness | workflow/harnesses/ + select-harness |
| Delegate a task to an agent | workflow/delegation/ |
| Find or add a skill | shared/skills/ |
| Look up stored context or research | knowledge/agent-wiki/ |
| Add a long-form note or article | knowledge/anytype-integration/ (AnyType PKM) |
| See my open commitments / tasks | AnyType → Tasks database |
| Run the default task loop | workflow/task-loop.md |
| Hand off or onboard a project | workflow/handoff/ |
| Bootstrap rules for a new project | shared/rules/ + shared/skills/rules-bootstrap/ |
| Review an agent's work | workflow/delegation/audit-log.md |
| Run a security review | governance/security-sandbox/ |
| Publish or document something | governance/docs-publish/ |
| Check metrics / friction log | instrumentation/ |
| Search my e-book library semantically | library/ (calibre-librarian, MCP) |
| Search Anthropic's published research | research/ (anthropic-research, MCP) |
Layout¶
.
├── shared/ Canonical tool-agnostic resources (skills, rules, agents)
├── knowledge/ Information stores
│ ├── agent-wiki/ Machine-readable context for agents
│ ├── anytype-integration/ AnyType PKM config and bridging
│ └── patterns/ Approved architecture patterns (markdown corpus)
├── code-library/ OSS clone catalog (lexical; category-gated)
├── workflow/ How work gets done
│ ├── task-loop.md Default bounded task loop
│ ├── harnesses/ Agent orchestration catalog
│ ├── delegation/ Agent delegation templates and audit log
│ └── handoff/ Project handoff protocol
├── governance/ Policies, audit, security sandbox, docs/publish
├── instrumentation/ Metrics and friction logging
├── library/ calibre-librarian — local RAG over a Calibre e-book library (MCP)
├── research/ anthropic-research — local RAG archive of Anthropic's published research (MCP)
├── patterns/ RAG + MCP over knowledge/patterns/
├── integrations/ Bridges between library/research and PKM (e.g. agent-wiki logging)
├── assets/ Static assets (icon, images)
├── scripts/ Config generation and drift-check scripts (see Configuration below)
├── Makefile Human catalog — `make` / `make help`
└── .cursor/ Cursor-specific config (references shared/ where possible)
Operating principles¶
- Single source of truth. Canonical assets live in
shared/. Tool configs reference it. - Capture everything, triage later. Use
pkm-captureor AnyType inbox; don't let things fall through. - Delegate with a paper trail. Every agent task gets a delegation template and an audit log entry.
- Privacy by default. Sensitive data stays local; cloud only with explicit opt-in.
- One capability at a time. Add skills when a real need surfaces, not speculatively.
Configuration¶
tool-chest.toml holds the shared structure and shipped defaults (MCP servers, clients,
capabilities, the sandbox). Your own choices (the Ollama and AnyType addresses, OmniRoute, the dashboard port, the
code-library source root and categories, the Calibre library, the whisper model, the research limits) live in your
private data root, one file per component: <data_root>/config/<component>.toml. Edit them in the workbench
(Settings) or with <component> config set; nothing in the dashboard writes tool-chest.toml. Each component
publishes its settings and their defaults (<component> config schema), and
workflow/config-inventory.toml lists what is shared and what is per-user. Shell
environment variables still override a value for one command.
Coming from the old layout (values in tool-chest.toml, or ~/.config/research, ~/.config/library)? Run
make config-migrate DRY=1 to see what would move, then make config-migrate. It keeps a backup and changes nothing
it has already moved.
Secrets never go in tool-chest.toml, and the AnyType key doesn't go in your shell profile either: anything
an agent runs inherits that. It lives in the macOS Keychain:
make anytype-key-store # paste the key (hidden); checked against the local API first
make anytype-key-check # stored? (reads no value, shows no dialog)
make anytype-key-delete
The item trusts no application, so every read shows a macOS Keychain dialog, even one by security itself.
Without that, any process able to run security, a sandboxed agent included, could read the key silently. You'll
see the dialog when Claude Code or Cursor starts the AnyType MCP (its launcher, scripts/anytype_mcp_launch.py, puts
the key only in that server's environment), when the dashboard first writes to AnyType, and on
make anytype-scope-check. The dialog calls the reader "security" in every case, so a dialog at any other moment is
one to deny. Click Allow, never "Always Allow", which would trust security and reopen the gap. The dashboard's
health ring never shows the dialog: it reports the key as stored but not yet read.
ANYTYPE_OPENAPI_MCP_HEADERS in the environment still works as an override, and it's how an automation hands its
own key to the command it runs. If you're moving from the old setup: make anytype-key-store, delete the export from
your shell profile, open a new terminal, and restart Claude Code / Cursor.
Rotating the key: issue a new one in AnyType, run make anytype-key-store (it replaces the item), revoke the old
key in AnyType, and restart Claude Code / Cursor.
To make the Anytype MCP URL available when Cursor starts, source the loader in your shell profile:
Private-names canary¶
This repo is public, so a client's, employer's or person's name must never land in a tracked file. make tenancy-check
fails if one does. The list of names to guard is one you write yourself, at <data_root>/canary.txt (by default
~/tool-chest-data/canary.txt): one name per line, blank lines and # comment lines ignored. Put in anyone or anything
you would not want named in a public repo: clients, people, employers. make canary-init creates the file from a
commented template that explains the format, if it doesn't exist (it never overwrites one). Run it on each machine:
the list is per machine, like the data root.
The list stays in the data root, which the repository refuses to contain, because a list of names that must not leak
would itself be the leak if it were committed. Matching ignores case and spacing, and a name only counts as a whole word
(Acme doesn't match Acmeville). A failure prints the tracked file and line, never the name. With no file the check
is skipped, which is what happens in CI: it guards your machine, not the pipeline.
MCP servers¶
Claude Code and Cursor read different files and use incompatible placeholder
syntax (${NAME} vs ${env:NAME}, ${CLAUDE_PROJECT_DIR} vs
${workspaceFolder}), so they cannot share one config. Instead, both are
generated from the [mcp.*] section of tool-chest.toml:
make mcp-config # regenerate .mcp.json and .cursor/mcp.json
make mcp-check # fail if either has drifted from tool-chest.toml
make mcp-install-user # write the user-level clients (claude-user via
# `claude mcp add`, ~/.cursor/mcp.json), so they load in any repo
make mcp-smoke # start each server, require initialize + tools/list
make mcp-check only compares config files; make mcp-smoke proves each
server actually starts (a crash inside serve passes every other gate). It is
slow on cold uv/npx caches and needs the Anytype app, so it is not part of
make check. Skip a server explicitly with
make mcp-smoke MCP_SMOKE_ARGS="--skip anytype"; CI does exactly that.
Add or change a server by editing tool-chest.toml and re-running
make mcp-config — never by editing the generated files, which
make mcp-check will flag. Restart the client for changes to attach.
npx servers run an exact version. The AnyType MCP server is npx = "@anyproto/anytype-mcp@2.0.1"
in tool-chest.toml, so a new upstream release changes nothing until you choose it. make mcp-check
fails on a range, a tag (latest) or a bare package name, and the launcher refuses to start an unpinned
one. To update: check the new version (npm view @anyproto/anytype-mcp version), change the pin, run
make mcp-config and make mcp-smoke, then commit. A pin fixes the version, not a hash of its
contents, so you still trust the registry's copy of that version.
AnyType write gate¶
The scoped API key limits what an agent can read; nothing limits what it writes.
scripts/hooks/anytype_write_gate.py is a Claude Code PreToolUse hook on the
AnyType MCP tools that applies each space's agent_write policy from
knowledge/anytype-integration/schemas/spaces.json: never is blocked with a
reason, approval shows the permission prompt every time, full passes. A write
whose space can't be determined, or a write tool that names no space, prompts.
Read tools are untouched, and any tool not on the read list is treated as a
write. The hook fails closed: an internal error becomes a prompt, never a silent
allow.
make anytype-gate-install # copy the gate to ~/.claude/hooks/tool-chest/ and register it in ~/.claude/settings.json (backs it up first)
make anytype-gate-check # fail if the installed copy or the registration differs from the repo
make claude-sandbox-install # also needed once: the deny_write rules that make that copy unwritable for agents
Session grants. To stop the prompts for a while, the approval prompt prints a
command to run in your own terminal, e.g. make anytype-grant SPACE=work TTL=30m.
Every prompt records its session in pending.json, and with no SESSION the
command takes the one session prompted for that space in the last 15 minutes (it
refuses, and lists them, if there are none or several; pass SESSION=<id> then).
A grant covers ordinary writes to that one space in that
one Claude Code session; the default is 30 minutes and requests over 2 hours are
clamped to 2 hours. Deletes and type or property changes always prompt. Each write
a grant allows is appended to audit.jsonl; make anytype-revoke SESSION=<id>
ends a grant early. State lives in <data_root>/anytype-grants/ (owner-only
files, never in the repo, expired records pruned on read). A resumed session keeps
its session id, so it keeps an unexpired grant and cannot reuse an expired one;
Claude Code has no "session ended" hook, so a grant ends at its TTL or on revoke.
The same hook denies shell calls that run the grant or revoke command, import its
module, or touch its state directory, and editor calls aimed at that directory, so
an agent cannot grant itself. That is a pattern match, not a sandbox. The boundary
is the install: the gate runs from a copy in ~/.claude/hooks/tool-chest/ (with a
snapshot of the space registry and the resolved state directory), and the
[claude_sandbox] deny_write globs stop agents writing that folder or
~/.claude/settings*.json, from Bash and from the editors. Editing the repo copy
changes nothing until you re-run the install, and make anytype-gate-check fails
while the two differ.
The gate also refuses shell commands that name the AnyType API port (from
anytype.api_base_url, baked into the installed copy) or ANYTYPE_API_BASE_URL, so
an agent can't skip the MCP tools with curl or python -c. Commit messages and
grep patterns that only mention the port are left alone. This too is a pattern
match, a backup behind the sandbox and the Keychain, not a boundary.
It only guards Claude Code. Cursor does not run these hooks, so keep the
anytype connection off or read-only for Cursor.
Claude Code sandbox¶
[claude_sandbox] in tool-chest.toml turns on Claude Code's built-in Bash
sandbox (macOS Seatbelt, nothing to install) with these rules:
- it unsets credential variables (
deny_env) before every sandboxed command; - it blocks reads of credential files and directories (
deny_files); - it lets sandboxed commands reach only the listed hosts (
allowed_domains). Commands may listen on local ports, which the test servers inmake checkneed. On macOS that also lets them reach local services (AnyType, OmniRoute, Ollama) on this machine: they're reachable, but without their keys.
A command the sandbox refuses can be retried outside it, but that retry always
asks first, as does every git push, in any permission mode.
make claude-sandbox-install # write it into ~/.claude/settings.json (backs it up first)
make claude-sandbox-check # in `make check`: fail if the installed copy drifted (skips without the file)
make claude-sandbox-uninstall # roll back: remove the block and our ask rules (backs up first)
This changes every Claude Code session on the machine, not only tool-chest's,
from the next session on. Rollback is make claude-sandbox-uninstall, or restore
one of the ~/.claude/settings.json.bak-* copies. The whole sandbox key belongs
to the installer: edit tool-chest.toml and reinstall rather than hand-editing it.
The sandbox covers Bash only. The Read and Edit tools and WebFetch follow
permission rules instead, and MCP servers aren't sandboxed, since Claude Code
launches them itself. Anything run from Claude that has to read OmniRoute's key
store (make omniroute-*) goes through the retry prompt. ps can't run in the
sandbox, so the one test that needs it prints a SKIP there and runs in full
from your own terminal.
To check that the defenses hold, make security-attack drives headless Claude
Code through attack probes and judges each by its effect on the machine. It
calls a model, so it isn't part of make check or CI. Run it from your own terminal, not from inside
a sandboxed Claude session: it has to see the real environment to detect a leak. To try a sandbox change
before installing it, pass the new block with
ATTACK_ARGS="--settings <file>"; scripts/claude_sandbox_install.py --print
writes the current one.
Clients are a registry: each [client.<id>] names a config path and a
format (claude-json, cursor-json, or the claude-cli adapter), and each
server lists the clients it is connected to in clients = [...]. Project
clients (path under @root/: claude-project, cursor) are generated whole
and drift-checked; user clients (claude-user, cursor-user) live outside the
repo and are written by make mcp-install-user. claude-user goes through
Claude Code's own CLI (claude mcp add-json / remove --scope user), never a
direct write to ~/.claude.json: connected servers are (re)registered, and a
server removed from its clients is removed from user scope — servers
tool-chest doesn't declare are never touched. ~/.cursor/mcp.json is merged
(entries tool-chest doesn't declare are kept), atomically, with the previous
file kept as mcp.json.bak. Adding a client whose file shape matches an existing
format needs only a [client.<id>] entry; a server naming an unknown client
fails make mcp-check.
A server can't be connected to both claude-project and claude-user
(shadows on the client): Claude Code resolves name collisions as
local > project > user, so the project entry would shadow the user one rather
than fall back to it. make mcp-check fails on such a pair.
The dashboard's Home ring connects areas to clients with one toggle per
registered client: it rewrites the area's servers' clients, syncs only that
client, and writes tool-chest.toml last. A client shows restart pending
while any of its processes started before its config last changed; it clears
itself once they've all restarted, and there is nothing to dismiss.
Shared rules¶
.cursor/rules/ is composed from shared/rules/ according to
rules-manifest.yaml (validated against
shared/rules/bootstrap/manifest.schema.json):
make rules-sync # re-render .cursor/rules/ from the manifest
make rules-check # fail if .cursor/rules/ has drifted from the manifest
Edit rules in shared/rules/, or change which slices this project inherits in
rules-manifest.yaml, then re-run make rules-sync. Never hand-edit
.cursor/rules/ — the entries are symlinks, and make rules-check flags a
file that has become a copy or that no manifest layer declares.
The manifest's rendered.mode is symlink here rather than copy, since the
composed files are byte-identical to their sources: drift is then structurally
impossible instead of merely detected. This matches .cursor/skills/, which
already symlinks into shared/skills/.
make check runs every drift gate (mcp-check + rules-check + docs-config-check + tenancy-check + help-check + capabilities-check + claude-sandbox-check), and
make hooks installs the pre-commit hook that runs it. Do that once per clone.
Tool setup¶
- Claude Code: Skills at
~/.claude/skills/; definitions documented inshared/skills/ - Cursor: Skills at
.cursor/skills/; rules at.cursor/rules/(both symlinked fromshared/) - AnyType: Local instance with MCP access; config at
knowledge/anytype-integration/
Phase roadmap¶
| Phase | What | Status |
|---|---|---|
| A–B | Structural reorganization + skills in shared/ |
done |
| Wave 0 | Tenancy, hybrid workspaces, host labs | done |
| Wave 1 | Patterns RAG, OmniRoute toggle, harness catalog | done |
| Wave 2 | Dashboard, voice, code library, sandbox plan | done |
| Next | Dashboard UX grill; AnyType Tasks depth | next |
Icon Attribution & Credit: Toolbox by Muhammad Nur Auliady Pamungkas from Noun Project (CC BY 3.0)