typeclaw 0.9.2 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -29,21 +29,27 @@ export type BuiltinRoleSpec = {
29
29
  readonly permissions: readonly string[]
30
30
  }
31
31
 
32
- // Owner carries low + medium tier strings explicitly AND the wildcard
33
- // sentinel. The sentinel expands to plugin-contributed `security.bypass.*`
34
- // strings minus the security plugin's `ownerWildcardExclusions` (today:
35
- // `security.bypass.high` plus high-tier per-guard strings). Net effect:
36
- // owner auto-bypasses every low- and medium-tier guard, and high-tier
37
- // guards require per-call ack from owner too (the audience-leak rule —
38
- // owner-in-public-channel must not silently post credentials).
32
+ // Role-to-tier defaults form a strict tower:
33
+ // owner → bypass.low + bypass.medium + bypass.high
34
+ // trusted bypass.low + bypass.medium
35
+ // member → bypass.low
36
+ // guest → no bypass
39
37
  //
40
- // Trusted carries only `security.bypass.low`. Trusted does NOT carry the
41
- // pre-PR per-guard grants (`bypassSecretExfilBash`, `bypassGitExfil`):
42
- // those guards are medium/high under the audience-leak axis and per-guard
43
- // grants would re-introduce exactly the bypass holes the tier system
44
- // exists to prevent. Operators who want the pre-PR ergonomics can add the
45
- // per-guard strings explicitly to `roles.trusted.permissions[]` in
46
- // typeclaw.json that path stays alive forever.
38
+ // `canBypass` in the bundled security plugin checks the specific tier
39
+ // string for the guard's severity, so each role must carry every tier
40
+ // string at or below its cap (tiers do not cascade implicitly).
41
+ //
42
+ // Owner also carries the wildcard sentinel: the sentinel expands to every
43
+ // plugin-contributed `security.bypass.*` string minus
44
+ // `ownerWildcardExclusions`. The bundled security plugin no longer excludes
45
+ // high-tier strings (owner is meant to bypass them by default under this
46
+ // model), so the sentinel covers per-guard high-tier strings too.
47
+ //
48
+ // Tradeoff: this gives owner audience-leak bypass without per-call ack.
49
+ // The owner-in-public-channel risk is now load-bearing on the operator
50
+ // scoping `roles.owner.match[]` tightly. Default match is TUI-only, where
51
+ // a human is present; configs that widen owner to a channel author should
52
+ // understand they have re-opened audience-leak for that author.
47
53
  export const BUILTIN_ROLES: Readonly<Record<BuiltinRoleName, BuiltinRoleSpec>> = {
48
54
  owner: {
49
55
  match: [{ kind: 'tui' }],
@@ -57,6 +63,7 @@ export const BUILTIN_ROLES: Readonly<Record<BuiltinRoleName, BuiltinRoleSpec>> =
57
63
  CORE_PERMISSIONS.subagentSpawnOperator,
58
64
  'security.bypass.low',
59
65
  'security.bypass.medium',
66
+ 'security.bypass.high',
60
67
  OWNER_SECURITY_WILDCARD,
61
68
  ],
62
69
  },
@@ -70,6 +77,7 @@ export const BUILTIN_ROLES: Readonly<Record<BuiltinRoleName, BuiltinRoleSpec>> =
70
77
  CORE_PERMISSIONS.subagentOutput,
71
78
  CORE_PERMISSIONS.subagentSpawnOperator,
72
79
  'security.bypass.low',
80
+ 'security.bypass.medium',
73
81
  ],
74
82
  },
75
83
  member: {
@@ -79,6 +87,7 @@ export const BUILTIN_ROLES: Readonly<Record<BuiltinRoleName, BuiltinRoleSpec>> =
79
87
  CORE_PERMISSIONS.subagentSpawn,
80
88
  CORE_PERMISSIONS.subagentCancel,
81
89
  CORE_PERMISSIONS.subagentOutput,
90
+ 'security.bypass.low',
82
91
  ],
83
92
  },
84
93
  guest: {
@@ -88,13 +97,12 @@ export const BUILTIN_ROLES: Readonly<Record<BuiltinRoleName, BuiltinRoleSpec>> =
88
97
  }
89
98
 
90
99
  // Expands the owner wildcard sentinel against plugin-contributed
91
- // `security.bypass.*` strings. `wildcardExclusions` is an optional set of
92
- // permission strings the sentinel must NOT expand to used by the
93
- // bundled security plugin to exclude `security.bypass.high` AND the
94
- // per-guard strings for high-tier guards, so the wildcard does not
95
- // auto-grant audience-leak bypass to owner. Explicit operator grants of
96
- // those strings in `roles.owner.permissions[]` still take effect (they
97
- // flow through the non-sentinel branch).
100
+ // `security.bypass.*` strings. `wildcardExclusions` lets plugins opt
101
+ // specific strings OUT of the wildcard expansion. The bundled security
102
+ // plugin no longer excludes any high-tier strings — owner bypasses every
103
+ // security tier by default under the current role-tower model. The
104
+ // parameter is preserved for third-party plugins that want a different
105
+ // shape (e.g. a future audit-only plugin that never auto-flows to owner).
98
106
  export function expandOwnerWildcard(
99
107
  ownerPermissions: readonly string[],
100
108
  pluginContributed: readonly string[],
@@ -152,6 +152,29 @@ export function createPermissionService(opts: CreatePermissionServiceOptions = {
152
152
  }
153
153
  }
154
154
 
155
+ // Walk order: owner, trusted, custom roles (in REVERSE declaration order),
156
+ // member, guest. First role whose `match[]` covers the origin wins.
157
+ //
158
+ // Built-in tower: owner > trusted > member > guest. Pinning the tower
159
+ // ahead of any user-declared rule closes a load-bearing footgun in the
160
+ // previous pure-declaration-order resolver: declaring
161
+ // `member.match: ["*"]` before `owner.match: [...]` resolved every
162
+ // channel session — INCLUDING the owner's — to `member`, because the
163
+ // wildcard matched first. The rolePromotion guard then made it
164
+ // un-fixable from inside the demoted session (a member-resolved speaker
165
+ // cannot rewrite `roles` without a TUI-issued ack).
166
+ //
167
+ // Custom roles use REVERSE declaration order: later declarations override
168
+ // earlier ones. This matches the standard "later config wins" mental
169
+ // model — when an operator adds a new role with the same match-scope as
170
+ // an existing one (or appends a new author-pinned override to an existing
171
+ // broad rule), the newer entry takes precedence. The previous "earlier
172
+ // wins" was an arbitrary consequence of map iteration order rather than
173
+ // a deliberate semantic.
174
+ //
175
+ // Custom roles cannot self-promote above trusted (no inherent severity
176
+ // guarantee) and cannot demote themselves below member (declaring a custom
177
+ // role implies the operator wants it to win against bottom catch-alls).
155
178
  function buildRoleTable(
156
179
  roles: RolesConfig,
157
180
  pluginPermissions: readonly string[],
@@ -160,16 +183,20 @@ function buildRoleTable(
160
183
  const out: ResolvedRole[] = []
161
184
  const seen = new Set<string>()
162
185
 
163
- for (const name of Object.keys(roles)) {
164
- if (seen.has(name)) continue
186
+ const emit = (name: string): void => {
187
+ if (seen.has(name)) return
165
188
  seen.add(name)
166
189
  out.push(resolveOne(name, roles[name], pluginPermissions, ownerWildcardExclusions))
167
190
  }
168
191
 
169
- for (const name of BUILTIN_ROLE_NAMES) {
170
- if (seen.has(name)) continue
171
- out.push(resolveOne(name, undefined, pluginPermissions, ownerWildcardExclusions))
192
+ emit('owner')
193
+ emit('trusted')
194
+ const customRoles = Object.keys(roles).filter((name) => !isBuiltinRoleName(name))
195
+ for (let i = customRoles.length - 1; i >= 0; i--) {
196
+ emit(customRoles[i]!)
172
197
  }
198
+ emit('member')
199
+ emit('guest')
173
200
 
174
201
  return out
175
202
  }
@@ -1,17 +1,17 @@
1
1
  import { randomBytes } from 'node:crypto'
2
2
 
3
3
  // Role-claim codes are short, human-typeable tokens the operator sends from
4
- // their host CLI to the bot via a channel DM to prove ownership of that
5
- // channel identity. Shape: `claim-XXXX-YYYY` where each block is 4 chars
6
- // from a Crockford-style base32 alphabet (0-9 + A-Z minus I, L, O, U to
7
- // dodge OCR-confusable / profane shapes). 8 chars * 5 bits = 40 bits of
8
- // entropy, which is overkill for a TTL'd in-memory window but cheap to
9
- // display and dictate over voice.
4
+ // their host CLI to the bot in any chat (DM, group, channel) to prove
5
+ // ownership of that channel identity. Shape: `claim-XXXX-YYYY` where each
6
+ // block is 4 chars from a Crockford-style base32 alphabet (0-9 + A-Z minus
7
+ // I, L, O, U to dodge OCR-confusable / profane shapes). 8 chars * 5 bits =
8
+ // 40 bits of entropy, which is overkill for a TTL'd in-memory window but
9
+ // cheap to display and dictate over voice.
10
10
  //
11
11
  // The `claim-` prefix lets the channel router recognize potential claim
12
- // attempts in a DM body without scanning the whole text for hex blocks,
13
- // and distinguishes claim DMs from normal first-message text like "hi"
14
- // which would otherwise need a regex of its own to disambiguate.
12
+ // attempts in inbound text without scanning the whole body for hex blocks,
13
+ // and distinguishes claim messages from normal first-message text like
14
+ // "hi" which would otherwise need a regex of its own to disambiguate.
15
15
 
16
16
  export const CLAIM_CODE_PREFIX = 'claim-'
17
17
 
@@ -10,8 +10,9 @@ import { createPendingClaimRegistry, type PendingClaim, type PendingClaimRegistr
10
10
  //
11
11
  // 1. The host CLI (typeclaw role claim) opens a WS and sends `claim_start`.
12
12
  // 2. The WS server forwards that to controller.startClaim().
13
- // 3. The channel router's claimHandler (also wired here) intercepts DMs
14
- // bearing the code and calls controller.tryConsumeInbound().
13
+ // 3. The channel router's claimHandler (also wired here) intercepts any
14
+ // inbound bearing the code (DM, group, or channel) and calls
15
+ // controller.tryConsumeInbound().
15
16
  // 4. On consume, the controller writes to typeclaw.json#roles.<role>.match
16
17
  // via grantRole, then reloads the live PermissionService so the new
17
18
  // match rule takes effect without a container restart.
@@ -1,15 +1,19 @@
1
1
  // Builds a canonical match-rule DSL string from an inbound channel origin,
2
- // for the role table. Output shapes:
2
+ // for the role table. Output shape is always platform-wide + author:
3
3
  //
4
- // slack:T0123 author:U_ALICE
5
- // discord:9999 author:U_ALICE
6
- // telegram:42 author:U_ALICE
7
- // kakao:dm/<chatId> author:<authorId>
4
+ // slack:* author:<authorId>
5
+ // discord:* author:<authorId>
6
+ // telegram:* author:<authorId>
7
+ // kakao:* author:<authorId>
8
8
  //
9
- // The author qualifier is always emitted so a claim grants the specific
10
- // human, not the whole workspace. To grant the whole workspace, the
11
- // operator edits typeclaw.json by hand or runs a future `typeclaw role grant`
12
- // without --claim.
9
+ // "Platform-wide" means every chat the adapter sees on that platform
10
+ // DMs, group chats, and threads alike gated by the author qualifier so
11
+ // only this specific human is matched. The intent is: once an operator
12
+ // proves they control a channel identity (by sending a code to the bot),
13
+ // they keep their role wherever they speak from on the same platform. To
14
+ // scope tighter (e.g. one workspace, one chat), the operator edits
15
+ // typeclaw.json by hand; the claim flow is deliberately broad because
16
+ // re-claiming on every new chat would be tedious for the common case.
13
17
 
14
18
  import type { ChannelKey } from '@/channels/types'
15
19
 
@@ -31,14 +35,5 @@ const ADAPTER_TO_PLATFORM: Record<ChannelKey['adapter'], 'slack' | 'discord' | '
31
35
 
32
36
  export function formatClaimMatchRule(origin: PartialChannelOrigin): string {
33
37
  const platform = ADAPTER_TO_PLATFORM[origin.adapter]
34
- const authorQual = ` author:${origin.authorId}`
35
- if (origin.adapter === 'kakaotalk') {
36
- // Kakao has no workspace; routes use dm/group/open buckets. We can't
37
- // know which bucket from a partial origin alone (adapter-side classifies
38
- // it), so claim flows are restricted to DM and we emit the specific
39
- // chat-id form so the rule grants only this 1:1 conversation, not every
40
- // DM the agent is in.
41
- return `${platform}:dm/${origin.chat}${authorQual}`
42
- }
43
- return `${platform}:${origin.workspace}${authorQual}`
38
+ return `${platform}:* author:${origin.authorId}`
44
39
  }
@@ -21,8 +21,8 @@ export type PendingClaimRegistry = {
21
21
  cancel: (code: string) => boolean
22
22
  current: () => PendingClaim | null
23
23
  // Snapshot of consumption result without actually committing the grant.
24
- // The router calls this on every DM-shaped inbound; the grant only fires
25
- // when the result is 'consumed'.
24
+ // The router calls this on every claim-code-bearing inbound; the grant
25
+ // only fires when the result is 'consumed'.
26
26
  tryConsume: (
27
27
  code: string,
28
28
  origin: PartialChannelOrigin,
@@ -65,7 +65,7 @@ Why this works: `~/.codex/auth.json` is the canonical credential location for th
65
65
  1. Confirm with the user: "Do you have the `codex` CLI installed on your local machine and are you signed in to it with your ChatGPT subscription? If not, install with `npm install -g @openai/codex`, then `codex login` and complete the browser authorization."
66
66
  2. Once they confirm, instruct them: "On your machine, the file at `~/.codex/auth.json` now contains your credential. Paste its contents back to me — it's a small JSON object with an `OPENAI_API_KEY` field, OR a `tokens` object with `access_token` / `refresh_token` / `id_token`. Either shape is valid. Treat it like a password (the `access_token` if present is short-lived but the `refresh_token` is long-lived)."
67
67
  3. When they paste, **validate** before writing: it must `JSON.parse` cleanly into an object with at least one of `{ OPENAI_API_KEY, tokens }`. If neither field is present, refuse and ask again — the user may have pasted only a fragment.
68
- 4. **Write to `~/.codex/auth.json` inside the container**, not `.env`. Create `~/.codex/` first if missing. Use `acknowledgeGuards: { nonWorkspaceWrite: true }` (the `~/.codex/` dir is outside `workspace/`).
68
+ 4. **Write to `~/.codex/auth.json` inside the container**, not `.env`. Create `~/.codex/` first if missing. Use `acknowledgeGuards: { nonWorkspaceWrite: true }` (the `~/.codex/` dir is outside `workspace/`). `~/.codex/auth.json` is symlinked by typeclaw's entrypoint shim to a host-side persistent path, so this write survives container restarts and codex's in-place token refreshes "just work" — see `references/auth-flow.md` "Persistence across container restarts" for the full mechanism.
69
69
  5. **Verify** by re-reading the file and re-parsing.
70
70
  6. **Ask before restart** (same prompt as the API key path).
71
71
  7. On yes → call the `restart` tool. On no → `typeclaw restart` themselves when ready.
@@ -99,6 +99,17 @@ There's also a `codex login --device-auth` flag for headless / cross-machine cas
99
99
 
100
100
  11. **Done.** There is no auth scratch directory, no tmux session to tear down, no worktree. The OAuth path has the same on-disk footprint as the API-key path: one credential file under `$HOME`.
101
101
 
102
+ ### Persistence across container restarts
103
+
104
+ `~/.codex/auth.json` inside the container is a symlink — installed by `typeclaw`'s entrypoint shim on every boot — pointing at `/agent/.typeclaw/home/.codex/auth.json`, which lives in the bind-mounted agent folder on the host. Two consequences:
105
+
106
+ 1. **You only paste auth.json once.** After the first successful write, `typeclaw restart` (or `typeclaw stop && typeclaw start`) preserves the credential without any further user action. Re-pasting is only needed when the refresh token itself is revoked (the user `/logout`s from their ChatGPT account, or the token expires after ~one year of inactivity).
107
+ 2. **Codex's in-place refresh "just works."** When codex rotates tokens (it rewrites `auth.json` with a refreshed access token plus the updated refresh-token state), the write goes through the symlink and lands on the persistent host-side path. On the next container start the symlink resolves back to the same file, so refreshes compound across runs the same way they do on a persistent CI runner — this is the pattern OpenAI's own [Codex CI/CD auth guide](https://developers.openai.com/codex/auth/ci-cd-auth) prescribes.
108
+
109
+ You do not need to do anything to enable this — the symlink is unconditional and idempotent, installed before the agent process ever starts. The persistent directory is gitignored (`.typeclaw/home/` in the generated `.gitignore`) so the credential never enters version control even though it sits inside the agent folder.
110
+
111
+ If the user asks "won't I lose auth.json on restart?" the answer is "no — it's symlinked to a host-side path; only `workspace/` and the container's regular `$HOME` are ephemeral."
112
+
102
113
  ## Failure modes on the user's side
103
114
 
104
115
  These all surface as the user's reply being an error message instead of a JSON object. Recognize them, do not validate them as credentials, and respond with the matching guidance.
@@ -126,6 +137,8 @@ These all surface as the user's reply being an error message instead of a JSON o
126
137
  - **Do not advise the user to `typeclaw shell` and run `codex login` inside the container as a "fallback".** It does not work — the container has no browser. Use the user-machine flow.
127
138
  - **Do not assume the `auth.json` schema.** OpenAI may change the shape — today's keys are `OPENAI_API_KEY` and `tokens.{access_token, refresh_token, id_token}`, but future versions may add or rename fields. Validate by presence of at least ONE known key, not by exhaustive shape match.
128
139
  - **Do not write to `~/.codex/auth.json` without `acknowledgeGuards: { nonWorkspaceWrite: true }`.** Same guard contract as `.env` writes.
129
- - **Do not patch-edit `~/.codex/auth.json`.** Read-modify-write the whole file (or just write the new contents wholesale — there's nothing to preserve from a stale `auth.json` on a fresh delegation).
140
+ - **Do not patch-edit `~/.codex/auth.json`.** Read-modify-write the whole file (or just write the new contents wholesale — there's nothing to preserve from a stale `auth.json` on a fresh delegation). Note that `~/.codex/auth.json` is a symlink into `/agent/.typeclaw/home/.codex/auth.json` — your write follows the link automatically and lands at the persistent host-side path, which is exactly what you want.
141
+ - **Do not move, delete, or replace the `~/.codex/auth.json` symlink with a real file.** The entrypoint shim re-establishes it on every container start, so any in-container `mv`/`cp`/`rm -f && touch` against the link is at best wasted work and at worst loses the credential on the next restart. Write THROUGH the symlink instead.
142
+ - **Do not write directly to `/agent/.typeclaw/home/`.** That path is a system-owned directory managed by the entrypoint shim. Use the `$HOME`-side path (`~/.codex/auth.json`) and let the symlink do its job. The persistent root may move or be renamed across typeclaw versions; the `$HOME` path is the stable contract.
130
143
  - **Do not store `OPENAI_API_KEY` in `~/.codex/auth.json`'s `OPENAI_API_KEY` field as a workaround for env-var precedence.** If the user wants the API-key path, the canonical location is `.env`. Mixing the two creates ambiguity for the next person debugging an auth failure.
131
144
  - **Do not branch on local-vs-remote container topology.** The user-machine flow is the same whether the container is on the user's laptop or on a remote host — the user runs `codex login` on whatever local machine they're at, the credential works in either container.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: typeclaw-config
3
- description: "Read or edit typeclaw.json: model, port, mounts, plugins, channels (per-adapter engagement and history; access control lives in roles — see typeclaw-permissions), portForward (auto port forwarding policy), docker.file (tmux/gh/python/ffmpeg toggles + append), git.ignore.append. Also: any question about a default value or whether a behavior is already on by default — port forwarding, channel visibility, model choice, container packages (tmux/gh/python on by default; ffmpeg off), anything ending in 'by default', 'automatically', 'out of the box', 'do I need to configure', 'is X on', 'what does X default to', '기본값', '기본적으로', '자동으로', '디폴트'. MUST load before saying you do not know what X defaults to, or proposing to add a field whose default the user is asking about — most fields already default to the behavior the user expects (portForward defaults to forwarding every container LISTEN; tmux/gh/python are pre-installed in the container; no edit needed). Read it before touching typeclaw.json — strict schema, mix of live-reloadable and restart-required fields."
3
+ description: "Read or edit typeclaw.json: model, port, mounts, plugins, channels (per-adapter engagement and history; access control lives in roles — see typeclaw-permissions), portForward (auto port forwarding policy), docker.file (tmux/gh/python/ffmpeg toggles + append), git.ignore.append. Also: any question about a default value or whether a behavior is already on by default — port forwarding, channel visibility, model choice, container packages (tmux/gh/python on by default; ffmpeg off), anything ending in 'by default', 'automatically', 'out of the box', 'do I need to configure', 'is X on', 'what does X default to', '기본값', '기본적으로', '자동으로', '디폴트'. MUST load before saying you do not know what X defaults to, or proposing to add a field whose default the user is asking about — most fields already default to the behavior the user expects (portForward defaults to forwarding every container LISTEN; tmux/gh/python are pre-installed in the container; no edit needed). Also covers recommended host paths to mount for common use cases (voice memos for STT, screenshots, mail, iMessage, notes vaults, downloads) with macOS/Linux/WSL paths and TCC/Full-Disk-Access gotchas — load when user describes a use case like 'transcribe my voice memos', 'triage my mail', 'mount my notes', 'let you see my screenshots', or asks 'what should I mount?'. Read it before touching typeclaw.json — strict schema, mix of live-reloadable and restart-required fields."
4
4
  ---
5
5
 
6
6
  # typeclaw-config
@@ -126,6 +126,12 @@ The `mounts/` directory itself is **gitignored** in your agent folder. The mount
126
126
  1. **Read `typeclaw.json`**, list each mount: `name`, `path`, `readOnly`, `description`.
127
127
  2. Optionally `ls mounts/` to confirm what is actually present right now (a mount won't appear until the next `typeclaw start` after it was added).
128
128
 
129
+ ### Common host paths to recommend
130
+
131
+ When the user describes a use case rather than naming a path — "transcribe my voice memos", "triage my mail", "look at my screenshots", "search my notes" — consult `references/recommended-mounts.md` for the canonical path per macOS/Linux/WSL, the `readOnly` default, and the macOS TCC / Full-Disk-Access gotchas (Mail, Messages, Calendars, Contacts, Safari all need FDA granted to Docker Desktop / OrbStack on the host). The reference also covers anti-patterns specific to host paths (don't mount `~` or `~/.ssh/` wholesale, `/Volumes/` is fragile under ejection, iCloud Drive paths lazy-load and may surface as 0-byte stubs). These complement the schema/correctness anti-patterns in `## Things you must not do` below.
132
+
133
+ The reference is **a lookup table, not a wishlist** — recommending a path there is not a license to add the mount silently. The user still has to ask, you still follow the standard procedure (read file, check collisions, pick name, append, write, commit, restart-required), and you still surface the TCC/FDA requirement before promising the agent can read FDA-gated data.
134
+
129
135
  ## Channels
130
136
 
131
137
  `channels` configures which external messenger adapters are enabled and how the engagement layer should behave on each. **Access control lives in `roles`, not here** — to admit a chat, declare a role match-rule that covers it (see `typeclaw-permissions`). The shape is `channels: { "<adapter-id>": { engagement, history, enabled } }`. Today the adapters are `discord-bot`, `slack-bot`, `telegram-bot`, and `kakaotalk`.
@@ -0,0 +1,233 @@
1
+ # Recommended mounts — common host paths
2
+
3
+ Deep dive on what host paths are worth recommending when the user asks "what should I mount?" or, more often, when the user describes a use case ("I want you to transcribe my voice memos", "help me triage my mail", "look at my screenshots") and you need to know the canonical path, the `readOnly` default, and the platform-specific gotchas before editing `typeclaw.json`.
4
+
5
+ Read it when `SKILL.md`'s **Mounts** section sends you here. It is **not** a list of mounts to add silently — every recommendation here still requires the user to ask for it. Adding mounts the user did not request is a security surprise (see `SKILL.md` "Things you must not do").
6
+
7
+ ## How to use this file
8
+
9
+ When the user describes a use case:
10
+
11
+ 1. **Find the matching row below.** If the use case isn't here, fall through to the general procedure in `SKILL.md` (pick a kebab-case name, pick `readOnly`, append the entry, restart-required).
12
+ 2. **Read the row's platform-specific path.** Apple has moved several of these paths across macOS versions; pick the right one for the user's `sw_vers` (you can ask, or have them run it on the host).
13
+ 3. **Honor the `readOnly` default.** Most recommendations here lean `readOnly: true` because the use case is "give the agent eyes on this data" not "let the agent rewrite it." If the user explicitly wants read-write, flip it — but say so.
14
+ 4. **Surface the gotchas before writing.** TCC/Full Disk Access for protected paths, iCloud lazy-download for `Mobile Documents/`, ejection-fragility for `/Volumes/`. Saying "I'll add the mount — note that this path needs Full Disk Access granted to Docker Desktop, otherwise the bind mount succeeds but reads will fail with EPERM" is the whole point of this file.
15
+ 5. **Then follow the standard Mounts procedure** in `SKILL.md` (read file, check collisions, pick name, append, write, commit, restart-required).
16
+
17
+ ## macOS: Transparency, Consent, Control (TCC) and Full Disk Access
18
+
19
+ Several juicy macOS paths — Mail, Messages, Calendars (macOS 14+), Contacts, Safari, Reminders, Photos — are gated by macOS's **TCC** subsystem. Apple's rule: the application that opens the file needs to be in System Settings → Privacy & Security → **Full Disk Access** (FDA). For typeclaw, that application is **Docker Desktop** (or **OrbStack** if the user is on OrbStack), because Docker is what mounts the path into the container.
20
+
21
+ What this means operationally:
22
+
23
+ - **The mount itself always succeeds.** Docker's bind mount is a kernel-level bind, not a file-open. The agent will see the path appear at `mounts/<name>/` after `typeclaw restart`.
24
+ - **`ls` may show files, but `read` returns EPERM.** When FDA is not granted, the container process can stat the directory but fails on actual file `open(2)`. The agent's `read` tool surfaces this as "Permission denied" and the agent looks like it's lying about being able to read the mount.
25
+ - **There is no in-container fix.** FDA is a per-application macOS preference set on the host. The user has to open System Settings, find Docker Desktop / OrbStack in Full Disk Access, toggle it on, and **restart Docker** (toggling FDA does not retroactively re-permission a running Docker daemon).
26
+
27
+ When recommending an FDA-gated path, say so up front: "This requires you to grant Docker Desktop Full Disk Access in System Settings → Privacy & Security, then restart Docker. Without that, I'll see the directory but get EPERM on every file."
28
+
29
+ OrbStack users: same rule, OrbStack appears in the FDA list as "OrbStack Helper" or "OrbStack" depending on version.
30
+
31
+ The rows below mark FDA-gated paths with **"FDA"** in the TCC column.
32
+
33
+ ## Tier 1 — high signal, low friction
34
+
35
+ Use cases the user will ask about often, with paths that don't need TCC grants (or only need the standard Documents/Desktop prompts macOS already pops on first access).
36
+
37
+ ### Voice memos — pairs with the `stt` skill
38
+
39
+ | Platform | Path | `readOnly` | TCC | Notes |
40
+ | ------------- | ------------------------------------------------------------------------- | ---------- | --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
41
+ | macOS (12+) | `~/Library/Group Containers/group.com.apple.VoiceMemos.shared/Recordings` | `true` | — | The group container path. **Not** the old `~/Library/Application Support/com.apple.voicememos` (removed in macOS 12). Includes iCloud-synced recordings from iPhone Voice Memos. |
42
+ | macOS (older) | `~/Library/Application Support/com.apple.voicememos/Recordings` | `true` | — | Only on macOS 11 and earlier. If both paths exist on a newer machine, prefer the group container — the old one is a stale copy. |
43
+ | Linux | (no native equivalent) | — | — | If the user records via `pw-record` / `pactl`, point them at their chosen output dir. |
44
+ | WSL | `/mnt/c/Users/<name>/Documents/Sound recordings/` (Voice Recorder app) | `true` | — | Windows Voice Recorder's default. |
45
+
46
+ Pair with the `stt` skill — once mounted, the agent can transcribe meetings/notes via Soniox.
47
+
48
+ ### Screenshots — quick visual context
49
+
50
+ | Platform | Path | `readOnly` | TCC | Notes |
51
+ | ----------------------- | ----------------------------------------------------------------- | ---------- | --- | ---------------------------------------------------------------------------------------------------------------------------------- |
52
+ | macOS | `~/Desktop` (default) | `true` | — | macOS Screenshots default to Desktop. Mount the whole Desktop or filter narrower if the user has a lot of unrelated stuff there. |
53
+ | macOS (custom location) | Run `defaults read com.apple.screencapture location` on the host. | `true` | — | Common moved locations: `~/Pictures/Screenshots`, `~/Documents/Screenshots`. Ask the user to run the `defaults` command if unsure. |
54
+ | Linux (GNOME) | `~/Pictures/Screenshots` | `true` | — | Default for GNOME Screenshot, KDE Spectacle. |
55
+ | WSL | `/mnt/c/Users/<name>/Pictures/Screenshots/` | `true` | — | Windows Win+Shift+S → Snipping Tool save location. |
56
+
57
+ Useful with multimodal models: "look at this screenshot and explain the error."
58
+
59
+ ### Downloads — ad-hoc file ingestion
60
+
61
+ | Platform | Path | `readOnly` | TCC | Notes |
62
+ | -------- | -------------------------------- | ---------------- | --- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
63
+ | macOS | `~/Downloads` | `true` (usually) | — | "Process the file I just downloaded." Read-only by default — the agent doesn't normally need to write here. macOS may prompt the user once on first container access (TCC prompt for Downloads), but it's a normal user-acknowledged prompt, not FDA. |
64
+ | Linux | `~/Downloads` | `true` (usually) | — | XDG-spec location. |
65
+ | WSL | `/mnt/c/Users/<name>/Downloads/` | `true` (usually) | — | Windows default Downloads folder. |
66
+
67
+ ### Obsidian vault — second-brain workflows
68
+
69
+ **Highly recommended** for users on Obsidian. The storage is plain markdown files in a folder, which is exactly what mounts are good at (no opaque formats, no proprietary databases, no encryption layer). Triggers: "my Obsidian vault", "my second brain", "search my notes", "summarize my daily note", "my vault is on iCloud".
70
+
71
+ | Platform | Path | `readOnly` | TCC | Notes |
72
+ | --------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------- | --- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
73
+ | macOS — iCloud-synced | `~/Library/Mobile Documents/iCloud~md~obsidian/Documents/<VaultName>` | `false` (RW for editing) or `true` (RO for research) | — | The canonical Obsidian-on-Apple-devices path. The literal example to use when the user says "my Obsidian vault" without further qualifier — this is where Obsidian Sync and iCloud both land. **iCloud lazy-loads.** Files the user hasn't opened recently may appear as 0-byte stubs until iCloud materializes them; see the anti-patterns section. |
74
+ | macOS — local vault | `~/Documents/<VaultName>` or `~/Obsidian/<VaultName>` | as above | — | For users who keep their vault out of iCloud. No lazy-load gotcha. |
75
+ | macOS — Obsidian Sync | (same as the local vault path the user chose) | as above | — | Obsidian Sync syncs in place — the path is wherever the user pointed Obsidian at, NOT a separate sync directory. Ask if unsure. |
76
+ | Linux | `~/Documents/<VaultName>` or `~/Obsidian/<VaultName>` | as above | — | Linux Obsidian is the same Electron app; vault is wherever the user pointed it. |
77
+ | WSL | `/mnt/c/Users/<name>/Documents/<VaultName>` or `~/Documents/<VaultName>` on the WSL side | as above | — | If the vault lives on the Windows side, beware 9p slowness — large vaults (thousands of notes) are noticeably laggy on directory traversal. Also CRLF line endings if Windows-side editors touched the files. |
78
+
79
+ If the user wants the agent to actively edit notes (write daily logs, refile, link), use `readOnly: false`. If they want pure read access for question-answering or research synthesis, use `readOnly: true`. **Always ask which** — the difference is large in practice (a misconfigured RW mount can silently rewrite the user's notes; a misconfigured RO mount can't help them refile).
80
+
81
+ **Special files in an Obsidian vault** to be aware of: `.obsidian/` (per-vault config, plugins, themes — leave it alone unless the user is debugging an Obsidian setting), `.obsidian/workspace.json` (active panes, mutated constantly while Obsidian is open — don't write to it from the agent or you'll race Obsidian itself), `.trash/` (Obsidian's local trash, gitignored by most users).
82
+
83
+ ### Other personal notes vaults (plaintext, Logseq, etc.)
84
+
85
+ For users on plain markdown without Obsidian, on Logseq, or on a custom vault structure:
86
+
87
+ | Platform | Path | `readOnly` | TCC | Notes |
88
+ | -------- | ------------------------------------------------------- | ---------------------------------------------------- | --- | -------------------------------------------------------------------------------------------------------------------------- |
89
+ | macOS | `~/Documents/<VaultName>` or wherever the user keeps it | `false` (RW for editing) or `true` (RO for research) | — | If the user wants the agent to edit notes, `false`. If they only want the agent to read/search, `true`. Default to asking. |
90
+ | Linux | `~/Documents/<VaultName>` or wherever the user keeps it | as above | — | No iCloud quirks. |
91
+ | WSL | `/mnt/c/Users/<name>/Documents/<VaultName>` | as above | — | Be aware of CRLF line endings on files originating from Windows-side editors. |
92
+
93
+ For **Logseq**, the vault is plain markdown (or org-mode) — same shape as the table above, just point at the Logseq graph directory.
94
+
95
+ ### Code repo — already the canonical example
96
+
97
+ | Platform | Path | `readOnly` | TCC | Notes |
98
+ | -------- | -------------------------------------------------- | ----------------- | --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
99
+ | any | `~/workspace/<repo>` or wherever the user keeps it | `false` typically | — | The standard "let me code on X" use case. Already covered in the example in `SKILL.md`. The mount is the host repo, so commits inside `mounts/<repo>/` go to the host repo's history (not the agent folder). |
100
+
101
+ ## Tier 2 — privacy-sensitive, FDA-gated
102
+
103
+ These are powerful but cross a real privacy boundary. **Always recommend `readOnly: true`** unless the user has a specific reason to write. **Always surface the FDA requirement** before editing `typeclaw.json` — the user needs to grant Docker Desktop / OrbStack Full Disk Access on the host, or the mount will read EPERM on every file.
104
+
105
+ Treat the act of suggesting these as a moment to pause and confirm. They are answers to "I want the agent to triage my mail / search my iMessages / scan my contacts", not defaults.
106
+
107
+ ### macOS Mail — local mailboxes
108
+
109
+ | Platform | Path | `readOnly` | TCC | Notes |
110
+ | ---------------------------------- | ----------------------------------------------------------- | ---------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
111
+ | macOS 14+ (Sonoma, Sequoia, Tahoe) | `~/Library/Mail/V10` | `true` | **FDA** | The `V10` directory holds per-account `.mbox`-like dirs containing `.emlx` files (one per message). The format is parseable as RFC822 + a small Apple-specific binary plist trailer. |
112
+ | macOS 13 (Ventura) | `~/Library/Mail/V9` | `true` | **FDA** | Same shape, older version dir. |
113
+ | macOS ≤12 | `~/Library/Mail/V8` or earlier | `true` | **FDA** | Same shape. |
114
+ | Linux — Thunderbird | `~/.thunderbird/<profile>.default-release/Mail/` | `true` | — | `.msf` index + `mbox` files. No TCC. |
115
+ | Linux — Evolution | `~/.local/share/evolution/mail/` | `true` | — | Maildir layout. No TCC. |
116
+ | WSL — Outlook (Win32) | `/mnt/c/Users/<name>/AppData/Local/Microsoft/Outlook/*.pst` | `true` | — | PST is a proprietary binary format; the agent needs `libpff` or similar to read it. Not as plug-and-play as `.emlx`. Warn the user. |
117
+
118
+ If unsure which `V<n>` the user has, ask them to run `ls ~/Library/Mail/` on the host — there's usually only one.
119
+
120
+ ### macOS Mail downloads — saved attachments
121
+
122
+ Mail.app stores attachments the user explicitly opened or saved in a separate location from the `V<n>` tree. Useful when the user says "the attachment from that email" — `V10/`'s per-message `Attachments/` subdirs are lazy and may not have the file, but Mail Downloads is a flat dir of explicitly-saved files.
123
+
124
+ | Platform | Path | `readOnly` | TCC | Notes |
125
+ | -------- | ------------------------------------------------------------------ | ---------- | --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
126
+ | macOS | `~/Library/Containers/com.apple.mail/Data/Library/Mail Downloads/` | `true` | — | Sandboxed app container path. **No FDA needed** — the app container is owned by the user and accessible without TCC grant. Complements the `V10/` mount for users who triage by email. |
127
+
128
+ This is the path Mail.app uses when the user clicks "Save All Attachments" or drags an attachment to Finder. Files here are flat (PDF, docx, images, etc.) — no envelope/header parsing required, unlike `V10/`'s `.emlx` format.
129
+
130
+ ### macOS Reminders
131
+
132
+ Parallel to Calendar — the raw Reminders store on disk. **Prefer the Reminders API via AppleScript or `gws-` skills** when those work; this is the fallback for direct introspection.
133
+
134
+ | Platform | Path | `readOnly` | TCC | Notes |
135
+ | ----------- | ---------------------- | ---------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
136
+ | macOS 14+ | `~/Library/Reminders/` | `true` | **FDA** | TCC began gating Reminders alongside Calendar in macOS 14. SQLite-backed; mount the parent dir for WAL sidecars (`.db-wal`, `.db-shm`). |
137
+ | macOS ≤13 | `~/Library/Reminders/` | `true` | — (no FDA prompt yet) | Same path, no FDA needed. |
138
+ | Linux / WSL | — | — | — | No native equivalent. |
139
+
140
+ If the user is on a Google or Microsoft to-do system instead of native Reminders, those flow through their own APIs — this mount is specifically for native macOS Reminders.
141
+
142
+ ### iMessage history
143
+
144
+ | Platform | Path | `readOnly` | TCC | Notes |
145
+ | ----------- | ------------------------------------------------------------------ | ---------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
146
+ | macOS | `~/Library/Messages` (the **whole directory**, not just `chat.db`) | `true` | **FDA** | The history is SQLite at `chat.db`. **Mount the parent dir, not just the db file** — SQLite uses `chat.db-wal` and `chat.db-shm` sidecar files for write-ahead logging, and mounting just `chat.db` will give you a frozen or corrupt-looking snapshot. Attachments live under `Attachments/` inside the same dir. |
147
+ | Linux / WSL | — | — | — | No equivalent (iMessage is Apple-only). |
148
+
149
+ Tell the user: this exposes every iMessage thread on the device. Highly sensitive. Make sure they're sure.
150
+
151
+ ### macOS Calendar (raw CalDAV store)
152
+
153
+ | Platform | Path | `readOnly` | TCC | Notes |
154
+ | ----------- | --------------------- | ---------- | --------------------- | ---------------------------------------------------------------------- |
155
+ | macOS 14+ | `~/Library/Calendars` | `true` | **FDA** | TCC began gating Calendars at macOS 14. ICS-format files per calendar. |
156
+ | macOS ≤13 | `~/Library/Calendars` | `true` | — (no FDA prompt yet) | Same path, no FDA needed. |
157
+ | Linux / WSL | — | — | — | No native equivalent on these platforms. |
158
+
159
+ **Prefer the `gws-calendar` skill** if the user is on Google Workspace — it's API-backed, no FDA, and writes are first-class. This raw mount is for users on iCloud-only or CalDAV (e.g. Fastmail) calendars where Google's API doesn't reach.
160
+
161
+ ### macOS Contacts
162
+
163
+ | Platform | Path | `readOnly` | TCC | Notes |
164
+ | ----------- | ------------------------------------------- | ---------- | ------- | --------------------------------------------------------------------------------------------------------- |
165
+ | macOS | `~/Library/Application Support/AddressBook` | `true` | **FDA** | SQLite + per-source dirs. Same warning as iMessage on `.db-wal`/`.db-shm` sidecars — mount the whole dir. |
166
+ | Linux / WSL | — | — | — | No native equivalent. |
167
+
168
+ Pairs awkwardly with `gws` Google Contacts if the user syncs both — there will be two sources of truth. Ask what they actually want before mounting.
169
+
170
+ ### Safari bookmarks / reading list
171
+
172
+ | Platform | Path | `readOnly` | TCC | Notes |
173
+ | ----------- | ------------------ | ---------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
174
+ | macOS | `~/Library/Safari` | `true` | **FDA** | `Bookmarks.plist` (binary plist, parseable with `plutil -convert json` if `plutil` is available, or with a Node plist parser inside the container). Reading list is in the same file. History is in `History.db` (SQLite). |
175
+ | Linux / WSL | — | — | — | No Safari on these platforms. For Chrome/Firefox bookmarks see below. |
176
+
177
+ ### Browser bookmarks (cross-platform)
178
+
179
+ | Platform | Path | `readOnly` | TCC | Notes |
180
+ | --------------- | ----------------------------------------------------------------------------- | ---------- | --- | ----------------------------------------------------------------------- |
181
+ | macOS — Chrome | `~/Library/Application Support/Google/Chrome/Default/Bookmarks` | `true` | — | JSON file. No FDA. |
182
+ | Linux — Chrome | `~/.config/google-chrome/Default/Bookmarks` | `true` | — | Same JSON format. |
183
+ | WSL — Chrome | `/mnt/c/Users/<name>/AppData/Local/Google/Chrome/User Data/Default/Bookmarks` | `true` | — | Same JSON. |
184
+ | macOS — Firefox | `~/Library/Application Support/Firefox/Profiles/<profile>/places.sqlite` | `true` | — | SQLite. Mount the parent profile dir, not just the file (WAL sidecars). |
185
+
186
+ ## Things that look mountable but aren't (recommend export)
187
+
188
+ Apps whose storage is opaque, proprietary, or actively-mutated-by-the-app-while-running. For all of these, the **clean recommendation is "export from the app and mount the export"**, not "mount the raw store." A few are technically parseable raw, but the schema can shift between app versions and the read-side complexity rarely earns its keep.
189
+
190
+ - **Apple Notes** (`~/Library/Group Containers/group.com.apple.notes/`) — stored as encrypted SQLite (`NoteStore.sqlite`) with content in protobuf blobs. Even with FDA, the format is not usefully readable without Apple's notes-decryption logic. The clean path is **export from Apple Notes app** (File → Export → PDF/HTML) into a regular directory, then mount that. Alternative: AppleScript / `osascript` driven from the host. Don't recommend mounting the raw container.
191
+ - **Photos library** (`~/Pictures/Photos Library.photoslibrary`) — a `.photoslibrary` package containing SQLite metadata and a content-addressed blob store. Mountable, but reading photos by user-visible name requires querying `Photos.sqlite` and joining against the blob store. Recommend the user **export albums** to a flat dir, or use `osxphotos` on the host to dump metadata. Like Notes, the raw package isn't agent-friendly.
192
+ - **Ulysses** (`~/Library/Containers/com.ulyssesapp.mac/Data/Library/Application Support/Ulysses/`) — sheets stored in a custom binary-plus-text format keyed by UUIDs, not human-readable filenames. The format isn't documented. Recommend the user **export sheets** from Ulysses (File → Export → Markdown or Text) to a flat directory, then mount that.
193
+ - **Bear** (`~/Library/Group Containers/9K33E3U3T4.net.shinyfrog.bear/`) — SQLite at `Application Data/database.sqlite`. **Technically parseable**: the schema is documented enough that a determined agent can read notes directly (one row per note, plaintext markdown in a column). But the schema has changed across Bear major versions, the database is locked while Bear is running, and tag/attachment relationships are spread across multiple tables. Recommend **Bear's built-in export** (File → Export Notes → Markdown) for stability; reach for the raw SQLite only if the user specifically asks for live, in-place access and accepts the fragility.
194
+ - **Things 3** (`~/Library/Group Containers/JLMPQHK86H.com.culturedcode.ThingsMac/`) — SQLite at `Things Database.thingsdatabase/main.sqlite`. **Technically parseable**: third-party tools like `things.py` exist and the schema is community-documented. Same caveats as Bear: locked-while-running, schema-shifts-between-versions, multi-table joins required for projects/areas/tags. Recommend Things 3's URL scheme or AppleScript bridge for writes; raw SQLite only for read-only introspection where the user accepts the fragility.
195
+ - **macOS Mail attachments inside `V10/`** — these live inside the `V10/` tree but are split across per-message `.mbox` directories under `Attachments/`. They're materialized lazily when Mail downloads them; messages opened only briefly may have no local attachment. Telling the user "I'll grab the attachment from that email" only works for emails Mail.app has fully cached. **For explicitly-saved attachments, use the Mail Downloads mount above instead** — that path is flat and reliable.
196
+ - **Keychain** (`~/Library/Keychains/`) — encrypted blobs, useless to read directly, very sensitive. Never recommend.
197
+
198
+ ## Dev / workflow staples — minor but worth mentioning
199
+
200
+ Quick wins that solve common questions without being privacy-loaded. **The recurring pattern**: for dotfiles that pair a `config` file with a `credentials` file, mount the **config**, never the credentials — credentials belong in `.env` or the typeclaw secrets store, not in a bind mount.
201
+
202
+ | Use case | Path | `readOnly` | Notes |
203
+ | ---------------------------------------- | ------------------------------------------------------------------------ | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
204
+ | Shell history | `~/.zsh_history` or `~/.bash_history` (mount the file or its parent dir) | `true` | "What command did I run yesterday?" Mounting the parent (`~`) is **not** the answer — mount the specific file. |
205
+ | SSH **config** (NOT keys) | `~/.ssh/config` | `true` | Single file. Useful for "ssh me into the staging box you set up." **Never mount `~/.ssh/` wholesale** — private keys live there. |
206
+ | Git config | `~/.gitconfig` | `true` | Lets the agent see the user's identity, aliases, signing config. |
207
+ | AWS **config** (NOT credentials) | `~/.aws/config` | `true` | Profile names, region defaults, SSO start URLs. Useful for "what AWS profiles do I have?". **Never mount `~/.aws/` wholesale** — `credentials` lives next to `config` in the same dir and carries access keys. For actual AWS work, plumb credentials through `.env` (`AWS_ACCESS_KEY_ID`, `AWS_PROFILE`, etc.), not a bind mount. |
208
+ | Kubernetes config | `~/.kube/config` | `true` | "What clusters do I have access to?" **Caveat**: depending on the auth method, `~/.kube/config` itself can contain bearer tokens or certificate data inline (older OIDC flows, EKS pre-IAM-auth, direct service-account tokens). Read the file before recommending — if it has inline tokens, treat it like a credential. |
209
+ | Editor configs (nvim, fish, helix, etc.) | `~/.config/nvim/`, `~/.config/fish/`, `~/.config/helix/` | `true` | "Why is my prompt broken?" / "What plugins do I have?" Generally safe (these dirs don't carry secrets), but check before recommending — some users put API keys in nvim plugin configs. |
210
+ | `/etc/hosts` | `/etc/hosts` | `true` | "What's in my hosts file?" Single-file mount. **Linux/macOS only.** WSL has a Windows-side hosts file at `/mnt/c/Windows/System32/drivers/etc/hosts` that takes precedence over the WSL-internal one — mount whichever the user actually edits. |
211
+ | macOS LaunchAgents (user-level) | `~/Library/LaunchAgents/` | `true` | "What auto-starts on my Mac?" Plist files declaring user-scope launchd jobs. System-level `/Library/LaunchAgents/` and `/Library/LaunchDaemons/` exist too but are SIP-adjacent and rarely useful. |
212
+ | Time Machine-excluded scratch | `~/scratch` (user creates with `tmutil addexclusion ~/scratch`) | `false` | Ephemeral agent output the user doesn't want backed up. |
213
+ | iCloud Drive top-level | `~/Library/Mobile Documents/com~apple~CloudDocs` | mixed | iCloud lazy-loads — same warning as the Obsidian iCloud row above. |
214
+
215
+ ## Anti-patterns
216
+
217
+ Append-only list, no overlap with `SKILL.md`'s existing anti-patterns (which cover schema/correctness, not host-path safety):
218
+
219
+ - **Never recommend mounting `~` (the entire home directory).** It's tempting and it's a security disaster: SSH keys, browser cookies, app credentials, `.env` files from every project, shell history with leaked tokens, Keychain blobs. If the user asks for "everything", push back and ask what they actually need.
220
+ - **Never mount `~/.ssh/` wholesale.** Private keys (`id_*`, `id_*.pub` ok but the bare files without `.pub` are private) should not enter the container. Mount the specific file `~/.ssh/config` read-only if the user wants SSH host visibility.
221
+ - **Never mount `~/Library/Keychains/`.** Even read-only, even with FDA. The keychain is encrypted blobs the agent can't use, and the act of bind-mounting it expands the attack surface for no benefit.
222
+ - **Never mount `~/.aws/`, `~/.gcp/`, `~/.azure/`, or any cloud-CLI credential dir.** Same reasoning as keychains — credentials, not data. If the user wants the agent to do cloud work, plumb credentials through `.env` (env vars the cloud SDK reads), not through a bind mount of the secret store. **The `config` file inside `~/.aws/` is fine to mount individually** (see the staples table) — it's the `credentials` file you have to keep out.
223
+ - **Never mount these credential-bearing dotfiles**, even though they look innocuous: `~/.aws/credentials`, `~/.npmrc` (often carries `//registry.npmjs.org/:_authToken=...` for private packages), `~/.pip/pip.conf` and `~/.pypirc` (PyPI upload tokens), `~/.docker/config.json` (Docker registry auth tokens, sometimes inline), `~/.cargo/credentials` (crates.io token), `~/.gradle/gradle.properties` (often carries signing keys and repo creds), `~/.m2/settings.xml` (Maven repo passwords). These pair with safe config files in similar paths — mount the config file individually if the user needs it, never the credentials file, never the parent directory.
224
+ - **`/Volumes/<external-drive>` is fragile.** Bind mounts to removable drives don't recover when the drive is ejected — the container sees the path become an empty stub, and the only fix is `typeclaw restart` after the drive is reattached. Tell the user before mounting, and consider whether copying the data to an internal path is wiser.
225
+ - **iCloud Drive paths lazy-load.** `~/Library/Mobile Documents/com~apple~CloudDocs/` and the per-app `iCloud~<bundle>` dirs only materialize files when the host system opens them. Inside the container, an unmaterialized file appears as a 0-byte stub or a `.icloud` placeholder. The fix is host-side: have the user open the file (or `brctl download` it) before the agent tries to read it. The agent cannot trigger iCloud materialization from inside the container.
226
+ - **`/private/var/db/` and other system stores are not yours.** TCC database, Spotlight metadata, system logs — all gated by SIP (System Integrity Protection) on top of FDA, and none of them are useful to the agent. If the user asks for Spotlight-style search, the answer is `mdfind` from the host (out of scope for the container), not a mount.
227
+ - **Don't mount the same host path twice under different mount names.** Docker allows it, but the agent now has two views of the same data and writes through one are immediately visible through the other. It looks like a bug from the agent's side ("why did `mounts/notes-ro/foo.md` change when I wrote `mounts/notes-rw/foo.md`?"). Pick one mount per host path.
228
+ - **`readOnly: true` is not encryption-at-rest.** A read-only mount prevents the agent from writing back, but reads still happen through the kernel's normal file path — no obfuscation, no privacy gain beyond write-prevention. If the data is so sensitive that read-access itself is a problem, the answer is **don't mount it**, not "mount it read-only".
229
+ - **macOS path versioning will drift.** Apple has moved `~/Library/Mail/V<n>`, Voice Memos, and other paths across macOS versions. The paths in this file are good for macOS 12–26 as of this writing; if a future macOS reorganization breaks one, the symptom is `ls mounts/<name>` returning empty after a successful mount. Update this file, don't paper over it.
230
+
231
+ ## When the path you need isn't here
232
+
233
+ Fall through to the general procedure in `SKILL.md`. If it's a use case you've handled successfully and it generalizes (other users would benefit), update this file in the same edit — the recommended-mounts list is meant to grow over time as the agent learns common asks.