@agentworkforce/cli 0.8.0 → 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.
package/CHANGELOG.md CHANGED
@@ -14,6 +14,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
14
14
  - Persona source config supports `defaultCreateTarget` for the implicit create target.
15
15
  - `--version`/`-v` now prints the CLI package version.
16
16
 
17
+ ## [0.10.0] - 2026-05-08
18
+
19
+ ### Released
20
+
21
+ - v0.10.0
22
+
17
23
  ## [0.8.0] - 2026-05-07
18
24
 
19
25
  ### Added
package/README.md CHANGED
@@ -515,6 +515,10 @@ library has no `extends`.
515
515
  },
516
516
  "env": { … }, // union, local wins per key
517
517
  "mcpServers": { … }, // union by server name, local wins per key
518
+ "mount": { // Relayfile file scope; pattern arrays append
519
+ "ignoredPatterns": ["…"],
520
+ "readonlyPatterns": ["…"]
521
+ },
518
522
  "permissions": { // allow/deny union (dedup), mode replaces
519
523
  "allow": ["…"], "deny": ["…"], "mode": "default"
520
524
  },
@@ -617,6 +621,9 @@ Use this when you are not extending an existing persona:
617
621
  "deny": ["Bash(npm publish *)"],
618
622
  "mode": "default"
619
623
  },
624
+ "mount": {
625
+ "readonlyPatterns": ["*"]
626
+ },
620
627
  "tiers": {
621
628
  "best": {
622
629
  "harness": "codex",
@@ -729,12 +736,39 @@ either a literal string or an env reference. Two forms:
729
736
  Secrets therefore stay in your shell/keychain, not in files on disk — local
730
737
  persona JSON remains commit-safe as long as you only use references.
731
738
 
739
+ ## Relayfile mount rules
740
+
741
+ Interactive `claude` and `opencode` sessions run inside a Relayfile mount by
742
+ default. File visibility and writability are controlled by the persona's
743
+ `mount` block plus project-level dotfiles:
744
+
745
+ ```jsonc
746
+ {
747
+ "mount": {
748
+ "ignoredPatterns": ["secrets/**", ".env*"],
749
+ "readonlyPatterns": [
750
+ "*",
751
+ "!docs/",
752
+ "!docs/**"
753
+ ]
754
+ }
755
+ }
756
+ ```
757
+
758
+ - `ignoredPatterns` are omitted from the mount entirely.
759
+ - `readonlyPatterns` are copied into the mount but edits do not sync back.
760
+ - Patterns use gitignore semantics, so later `!` negations can reopen paths.
761
+ - Persona patterns append to inherited persona patterns. At launch, the CLI
762
+ also merges project-root `.agentignore`, `.agentreadonly`,
763
+ `.<personaId>.agentignore`, and `.<personaId>.agentreadonly`.
764
+
732
765
  ## Permissions
733
766
 
734
767
  A persona can declare which tool calls the harness should auto-approve, block,
735
768
  or gate via a permission mode. Skip the approval prompts for trusted tools
736
769
  (e.g. a persona's own MCP server); keep them on for anything you want to
737
- eyeball.
770
+ eyeball. File visibility and writability are not defined here; use Relayfile
771
+ mount rules (`.agentignore` / `.agentreadonly`) for that.
738
772
 
739
773
  ```jsonc
740
774
  {
@@ -747,9 +781,8 @@ eyeball.
747
781
  ```
748
782
 
749
783
  - **Tool patterns** are passed through verbatim; use the harness's native
750
- grammar. For Claude Code: `Bash(<pattern>)`, `Edit(<glob>)`,
751
- `mcp__<server>` (all tools from that server), `mcp__<server>__<tool>`
752
- (specific tool).
784
+ grammar. For Claude Code: `Bash(<pattern>)`, `mcp__<server>` (all tools
785
+ from that server), `mcp__<server>__<tool>` (specific tool).
753
786
  - **Harness support today:** only `claude` is wired (flags: `--allowedTools`,
754
787
  `--disallowedTools`, `--permission-mode`). codex and opencode emit a
755
788
  warning and fall back to their defaults when `permissions` is set.
@@ -922,13 +955,23 @@ stage dir conflicts with something else (network filesystem, read-only
922
955
 
923
956
  By default, claude and opencode interactive sessions run inside a
924
957
  [`@relayfile/local-mount`](https://www.npmjs.com/package/@relayfile/local-mount)
925
- symlink mount that hides repo-level harness configuration from the session
926
- and routes skill-install writes into the sandbox so the model sees
927
- persona context + user-level context, and nothing the repo itself declares.
928
- Codex sessions never mount (no harness-side support).
958
+ mount that hides repo-level harness configuration from the session, applies
959
+ the persona `mount` block plus Relayfile `.agentignore` / `.agentreadonly`
960
+ rules, and routes skill-install writes into the sandbox — so the model sees
961
+ persona context + user-level context, and only the project files the mount
962
+ exposes. Codex sessions never mount (no harness-side support).
929
963
 
930
964
  `--install-in-repo` opts out and runs against the real cwd.
931
965
 
966
+ The CLI reads these files from the project root before creating the mount:
967
+
968
+ | File | Effect |
969
+ | --- | --- |
970
+ | `.agentignore` | Hide matching files for every persona. |
971
+ | `.agentreadonly` | Copy matching files into the mount as read-only and skip syncing their edits back. |
972
+ | `.<personaId>.agentignore` | Hide matching files only for that persona. |
973
+ | `.<personaId>.agentreadonly` | Make matching files read-only only for that persona. |
974
+
932
975
  **What's hidden (gitignore semantics, at any depth):**
933
976
 
934
977
  For claude:
package/dist/cli.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { type Harness } from '@agentworkforce/workload-router';
2
+ import { type Harness, type PersonaMount, type PersonaSelection, type SidecarMdMode } from '@agentworkforce/workload-router';
3
3
  export declare const CLI_VERSION: string;
4
4
  export declare const CREATE_SELECTOR = "persona-maker@best";
5
5
  /**
@@ -40,7 +40,7 @@ export declare function assertSafeRelativePath(relPath: string): void;
40
40
  * Applied by `@relayfile/local-mount` with gitignore semantics, so bare names
41
41
  * match at any depth in the project tree (e.g. `.claude` hides both
42
42
  * `./.claude/` and `./packages/foo/.claude/`). */
43
- export declare const CLEAN_IGNORED_PATTERNS: readonly ["CLAUDE.md", "CLAUDE.local.md", ".claude", ".mcp.json"];
43
+ export declare const CLEAN_IGNORED_PATTERNS: readonly ["CLAUDE.md", "CLAUDE.local.md", ".claude", ".mcp.json", "AGENTS.md"];
44
44
  /**
45
45
  * Skill-install artifacts that should never be copied into the mount nor
46
46
  * synced back to the real repo. Applied to non-claude interactive sessions
@@ -51,7 +51,18 @@ export declare const CLEAN_IGNORED_PATTERNS: readonly ["CLAUDE.md", "CLAUDE.loca
51
51
  * Claude sessions use `installRoot` for out-of-repo staging instead, so
52
52
  * these patterns don't apply there.
53
53
  */
54
- export declare const SKILL_INSTALL_IGNORED_PATTERNS: readonly [".agents", ".claude/skills", ".factory/skills", ".kiro/skills", "skills", ".opencode", ".skills", "prpm.lock", "skills-lock.json"];
54
+ export declare const SKILL_INSTALL_IGNORED_PATTERNS: readonly [".agents", ".claude/skills", ".factory/skills", ".kiro/skills", "skills", ".opencode", ".skills", "prpm.lock", "skills-lock.json", "AGENTS.md"];
55
+ export interface RelayfileMountPatterns {
56
+ ignoredPatterns: string[];
57
+ readonlyPatterns: string[];
58
+ }
59
+ export declare function buildRelayfileMountPatterns(input: {
60
+ projectDir: string;
61
+ personaId: string;
62
+ harness: Harness;
63
+ mount?: PersonaMount;
64
+ configFilePaths?: readonly string[];
65
+ }): RelayfileMountPatterns;
55
66
  /**
56
67
  * Build the block appended to `<mount>/.git/info/exclude` so untracked-and-
57
68
  * hidden files (e.g. `.claude/skills/` materialized by skill installs, or
@@ -78,6 +89,37 @@ export declare function buildMountGitExcludeBlock(patterns: readonly string[]):
78
89
  * writes here are sandboxed and never leak to the user's main checkout.
79
90
  */
80
91
  export declare function configureGitForMount(mountDir: string, patterns: readonly string[]): void;
92
+ /**
93
+ * Persona-supplied sidecar markdown materialized into a sandbox mount.
94
+ * Pure data carrier — `runInteractive` translates it into the on-disk
95
+ * write inside `onBeforeLaunch` (mount path) and warns/skips when the
96
+ * harness has no mount (codex / `--install-in-repo`).
97
+ */
98
+ export interface ResolvedSidecar {
99
+ /** Filename inside the mount: `CLAUDE.md` (claude) or `AGENTS.md` (opencode). */
100
+ mountFile: 'CLAUDE.md' | 'AGENTS.md';
101
+ /** Persona-author content. Already inlined for built-ins; read from disk for local. */
102
+ personaContent: string;
103
+ mode: SidecarMdMode;
104
+ }
105
+ /**
106
+ * Resolve the sidecar for a given selection + harness, returning the
107
+ * persona-author content the runtime should materialize into the mount.
108
+ * Returns `{}` when no sidecar applies (no path/content set, or harness
109
+ * doesn't support sidecar files at all). Read errors surface as a warning
110
+ * string so the caller can drop the sidecar gracefully rather than
111
+ * failing the whole session.
112
+ */
113
+ export declare function loadSidecarForSelection(selection: PersonaSelection): {
114
+ sidecar?: ResolvedSidecar;
115
+ warning?: string;
116
+ };
117
+ /**
118
+ * Compute the bytes to write into the mount for a sidecar. In `extend`
119
+ * mode, prepends the user's real-cwd file (if any) joined to the persona
120
+ * content with `\n\n---\n\n`. Pure — exposed for unit tests.
121
+ */
122
+ export declare function buildSidecarBody(sidecar: ResolvedSidecar, realCwdDir: string): string;
81
123
  /**
82
124
  * Decide whether to run the interactive session inside a
83
125
  * `@relayfile/local-mount` sandbox.
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAgBA,OAAO,EAOL,KAAK,OAAO,EAMb,MAAM,iCAAiC,CAAC;AA8IzC,eAAO,MAAM,WAAW,QAAuB,CAAC;AAChD,eAAO,MAAM,eAAe,uBAAuB,CAAC;AAgDpD;;;;;;;;;;GAUG;AACH,wBAAgB,+BAA+B,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAExF;AA6ID;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAUhE;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAe5D;AAED;;;kDAGkD;AAClD,eAAO,MAAM,sBAAsB,mEAKzB,CAAC;AAEX;;;;;;;;;GASG;AACH,eAAO,MAAM,8BAA8B,8IAajC,CAAC;AAEX;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAU7E;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAuCxF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,OAAO,EAChB,aAAa,UAAQ,GACpB;IAAE,QAAQ,EAAE,OAAO,CAAA;CAAE,CAKvB;AAwmBD,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,kBAAkB,CA+B5E;AAolBD,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CA6D1C;AAED,MAAM,WAAW,UAAU;IACzB,aAAa,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,WAAY,SAAQ,UAAU;IAC7C,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG;IACvD,KAAK,EAAE,UAAU,CAAC;IAClB,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB,CAwBA;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG;IACxD,KAAK,EAAE,WAAW,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACrC,CA4DA"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
package/dist/cli.js CHANGED
@@ -5,9 +5,9 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync,
5
5
  import { constants, homedir } from 'node:os';
6
6
  import { dirname, isAbsolute, join, resolve as resolvePath } from 'node:path';
7
7
  import { pathToFileURL } from 'node:url';
8
- import { HARNESS_VALUES, PERSONA_TAGS, PERSONA_TIERS, personaCatalog, routingProfiles, useSelection } from '@agentworkforce/workload-router';
8
+ import { HARNESS_VALUES, PERSONA_TAGS, PERSONA_TIERS, personaCatalog, resolveSidecar, routingProfiles, useSelection } from '@agentworkforce/workload-router';
9
9
  import { buildInteractiveSpec, detectHarnesses, formatDropWarnings, MissingPersonaInputError, renderPersonaInputs, resolvePersonaInputs, resolveMcpServersLenient, resolveStringMapLenient } from '@agentworkforce/harness-kit';
10
- import { launchOnMount } from '@relayfile/local-mount';
10
+ import { launchOnMount, readAgentDotfiles } from '@relayfile/local-mount';
11
11
  import ora from 'ora';
12
12
  import { buildPersonaSourceDirectories, defaultCwdPersonaDir, loadLocalPersonas, loadPersonaSourceConfig, normalizePersonaDir, savePersonaSourceConfig } from './local-personas.js';
13
13
  import { installPersonas } from './persona-install.js';
@@ -187,6 +187,7 @@ function buildSelection(spec, tier, kind) {
187
187
  ...rawRuntime,
188
188
  systemPrompt: resolveSystemPromptPlaceholders(rawRuntime.systemPrompt, rawRuntime.harness)
189
189
  };
190
+ const sidecar = resolveSidecar(spec, tier);
190
191
  return {
191
192
  personaId: spec.id,
192
193
  tier,
@@ -196,7 +197,18 @@ function buildSelection(spec, tier, kind) {
196
197
  ...(spec.inputs ? { inputs: spec.inputs } : {}),
197
198
  ...(spec.env ? { env: spec.env } : {}),
198
199
  ...(spec.mcpServers ? { mcpServers: spec.mcpServers } : {}),
199
- ...(spec.permissions ? { permissions: spec.permissions } : {})
200
+ ...(spec.permissions ? { permissions: spec.permissions } : {}),
201
+ ...(spec.mount ? { mount: spec.mount } : {}),
202
+ ...(sidecar.claudeMd ? { claudeMd: sidecar.claudeMd } : {}),
203
+ ...(sidecar.claudeMdContent ? { claudeMdContent: sidecar.claudeMdContent } : {}),
204
+ ...(sidecar.claudeMd || sidecar.claudeMdContent
205
+ ? { claudeMdMode: sidecar.claudeMdMode }
206
+ : {}),
207
+ ...(sidecar.agentsMd ? { agentsMd: sidecar.agentsMd } : {}),
208
+ ...(sidecar.agentsMdContent ? { agentsMdContent: sidecar.agentsMdContent } : {}),
209
+ ...(sidecar.agentsMd || sidecar.agentsMdContent
210
+ ? { agentsMdMode: sidecar.agentsMdMode }
211
+ : {})
200
212
  };
201
213
  }
202
214
  function emitDropWarnings(lines) {
@@ -368,7 +380,12 @@ export const CLEAN_IGNORED_PATTERNS = [
368
380
  'CLAUDE.md',
369
381
  'CLAUDE.local.md',
370
382
  '.claude',
371
- '.mcp.json'
383
+ '.mcp.json',
384
+ // Per-persona AGENTS.md sidecars get materialized into the mount when
385
+ // running under opencode; without this the user's real-cwd AGENTS.md
386
+ // would copy in (masking the persona content) and writes from
387
+ // onBeforeLaunch would sync back out.
388
+ 'AGENTS.md'
372
389
  ];
373
390
  /**
374
391
  * Skill-install artifacts that should never be copied into the mount nor
@@ -392,8 +409,32 @@ export const SKILL_INSTALL_IGNORED_PATTERNS = [
392
409
  '.skills',
393
410
  // provider lockfiles written at the repo root
394
411
  'prpm.lock',
395
- 'skills-lock.json'
412
+ 'skills-lock.json',
413
+ // Per-persona AGENTS.md sidecars (opencode harness) get materialized
414
+ // into the mount; hide so the real-cwd AGENTS.md isn't copied in and
415
+ // the persona-written copy doesn't sync back out.
416
+ 'AGENTS.md'
396
417
  ];
418
+ export function buildRelayfileMountPatterns(input) {
419
+ const dotfiles = readAgentDotfiles(input.projectDir, {
420
+ agentName: input.personaId
421
+ });
422
+ const builtInIgnored = input.harness === 'claude'
423
+ ? CLEAN_IGNORED_PATTERNS
424
+ : SKILL_INSTALL_IGNORED_PATTERNS;
425
+ return {
426
+ ignoredPatterns: [
427
+ ...dotfiles.ignoredPatterns,
428
+ ...(input.mount?.ignoredPatterns ?? []),
429
+ ...builtInIgnored,
430
+ ...(input.configFilePaths ?? [])
431
+ ],
432
+ readonlyPatterns: [
433
+ ...dotfiles.readonlyPatterns,
434
+ ...(input.mount?.readonlyPatterns ?? [])
435
+ ]
436
+ };
437
+ }
397
438
  /**
398
439
  * Build the block appended to `<mount>/.git/info/exclude` so untracked-and-
399
440
  * hidden files (e.g. `.claude/skills/` materialized by skill installs, or
@@ -465,6 +506,96 @@ export function configureGitForMount(mountDir, patterns) {
465
506
  }
466
507
  }
467
508
  }
509
+ /**
510
+ * Resolve the sidecar for a given selection + harness, returning the
511
+ * persona-author content the runtime should materialize into the mount.
512
+ * Returns `{}` when no sidecar applies (no path/content set, or harness
513
+ * doesn't support sidecar files at all). Read errors surface as a warning
514
+ * string so the caller can drop the sidecar gracefully rather than
515
+ * failing the whole session.
516
+ */
517
+ export function loadSidecarForSelection(selection) {
518
+ const harness = selection.runtime.harness;
519
+ if (harness !== 'claude' && harness !== 'opencode')
520
+ return {};
521
+ if (harness === 'claude') {
522
+ if (selection.claudeMdContent) {
523
+ return {
524
+ sidecar: {
525
+ mountFile: 'CLAUDE.md',
526
+ personaContent: selection.claudeMdContent,
527
+ mode: selection.claudeMdMode ?? 'overwrite'
528
+ }
529
+ };
530
+ }
531
+ if (selection.claudeMd) {
532
+ try {
533
+ const content = readFileSync(selection.claudeMd, 'utf8');
534
+ return {
535
+ sidecar: {
536
+ mountFile: 'CLAUDE.md',
537
+ personaContent: content,
538
+ mode: selection.claudeMdMode ?? 'overwrite'
539
+ }
540
+ };
541
+ }
542
+ catch (err) {
543
+ return { warning: `claudeMd: could not read ${selection.claudeMd}: ${err.message}` };
544
+ }
545
+ }
546
+ return {};
547
+ }
548
+ if (selection.agentsMdContent) {
549
+ return {
550
+ sidecar: {
551
+ mountFile: 'AGENTS.md',
552
+ personaContent: selection.agentsMdContent,
553
+ mode: selection.agentsMdMode ?? 'overwrite'
554
+ }
555
+ };
556
+ }
557
+ if (selection.agentsMd) {
558
+ try {
559
+ const content = readFileSync(selection.agentsMd, 'utf8');
560
+ return {
561
+ sidecar: {
562
+ mountFile: 'AGENTS.md',
563
+ personaContent: content,
564
+ mode: selection.agentsMdMode ?? 'overwrite'
565
+ }
566
+ };
567
+ }
568
+ catch (err) {
569
+ return { warning: `agentsMd: could not read ${selection.agentsMd}: ${err.message}` };
570
+ }
571
+ }
572
+ return {};
573
+ }
574
+ /**
575
+ * Compute the bytes to write into the mount for a sidecar. In `extend`
576
+ * mode, prepends the user's real-cwd file (if any) joined to the persona
577
+ * content with `\n\n---\n\n`. Pure — exposed for unit tests.
578
+ */
579
+ export function buildSidecarBody(sidecar, realCwdDir) {
580
+ if (sidecar.mode === 'extend') {
581
+ const realPath = join(realCwdDir, sidecar.mountFile);
582
+ try {
583
+ const realContent = readFileSync(realPath, 'utf8');
584
+ return `${realContent}\n\n---\n\n${sidecar.personaContent}`;
585
+ }
586
+ catch (err) {
587
+ // Only "missing path" errors degrade to overwrite. Real I/O
588
+ // problems (EACCES, EISDIR, …) propagate so callers see them
589
+ // instead of silently dropping the user's CLAUDE.md/AGENTS.md.
590
+ const code = err.code;
591
+ if (code === 'ENOENT' || code === 'ENOTDIR') {
592
+ return sidecar.personaContent;
593
+ }
594
+ throw err;
595
+ }
596
+ }
597
+ return sidecar.personaContent;
598
+ }
468
599
  /**
469
600
  * Decide whether to run the interactive session inside a
470
601
  * `@relayfile/local-mount` sandbox.
@@ -500,6 +631,21 @@ async function runInteractive(selection, options = {}) {
500
631
  // below). The --install-in-repo flag forces legacy in-repo installs
501
632
  // across the board.
502
633
  const useClean = decideCleanMode(runtime.harness, options.installInRepo === true).useClean;
634
+ // Per-persona CLAUDE.md / AGENTS.md: load the author content if any. The
635
+ // file is materialized into the mount inside onBeforeLaunch (claude/
636
+ // opencode default). Without a mount (codex, --install-in-repo) we
637
+ // skip-and-warn — writing into the real cwd would pollute the user's
638
+ // repo and is explicitly out of scope.
639
+ const sidecarLookup = loadSidecarForSelection(effectiveSelection);
640
+ if (sidecarLookup.warning) {
641
+ process.stderr.write(`warning: ${sidecarLookup.warning}\n`);
642
+ }
643
+ const resolvedSidecar = useClean ? sidecarLookup.sidecar : undefined;
644
+ if (sidecarLookup.sidecar && !useClean) {
645
+ process.stderr.write(`warning: persona declares ${sidecarLookup.sidecar.mountFile} but no sandbox mount is available (` +
646
+ `${runtime.harness === 'codex' ? 'codex harness has no mount' : '--install-in-repo disengages the mount'})` +
647
+ `; skipping sidecar materialization to avoid writing into your repo.\n`);
648
+ }
503
649
  // A session dir is needed whenever we either (a) stage skills out-of-repo
504
650
  // via claude's installRoot, or (b) open a mount. Both engage for claude/
505
651
  // opencode by default; --install-in-repo disengages both.
@@ -608,9 +754,6 @@ async function runInteractive(selection, options = {}) {
608
754
  // copied in from the real repo nor synced back on exit.
609
755
  if (useClean && sessionRoot) {
610
756
  const mountDir = sessionMountDir(sessionRoot);
611
- const ignoredPatterns = runtime.harness === 'claude'
612
- ? [...CLEAN_IGNORED_PATTERNS]
613
- : [...SKILL_INSTALL_IGNORED_PATTERNS];
614
757
  // Anything we materialize into the mount via onBeforeLaunch must be
615
758
  // hidden from the mount-mirror in both directions: without this, any
616
759
  // opencode.json already present in the real repo would be copied into
@@ -618,9 +761,13 @@ async function runInteractive(selection, options = {}) {
618
761
  // fresh write from onBeforeLaunch would sync back out on exit and
619
762
  // pollute the user's working tree. Added dynamically so this stays
620
763
  // generic for any future configFile producer.
621
- for (const file of spec.configFiles) {
622
- ignoredPatterns.push(file.path);
623
- }
764
+ const { ignoredPatterns, readonlyPatterns } = buildRelayfileMountPatterns({
765
+ projectDir: process.cwd(),
766
+ personaId,
767
+ harness: runtime.harness,
768
+ mount: effectiveSelection.mount,
769
+ configFilePaths: spec.configFiles.map((file) => file.path)
770
+ });
624
771
  process.stderr.write(`• sandbox mount → ${mountDir}\n`);
625
772
  // Three-stage SIGINT handler layered on top of launchOnMount's own signal
626
773
  // forwarding. launchOnMount catches the first SIGINT to kill the child
@@ -696,6 +843,7 @@ async function runInteractive(selection, options = {}) {
696
843
  // `git push` to persist work — local-only commits evaporate with
697
844
  // the session.
698
845
  includeGit: true,
846
+ readonlyPatterns,
699
847
  // Second Ctrl-C aborts this signal → local-mount skips autosync's
700
848
  // draining reconcile and returns the partial syncBack count. Cleanup
701
849
  // still runs, so there's no leaked mount dir.
@@ -745,6 +893,10 @@ async function runInteractive(selection, options = {}) {
745
893
  mkdirSync(dirname(target), { recursive: true });
746
894
  writeFileSync(target, file.contents, 'utf8');
747
895
  }
896
+ if (resolvedSidecar) {
897
+ const body = buildSidecarBody(resolvedSidecar, process.cwd());
898
+ writeFileSync(join(dir, resolvedSidecar.mountFile), body, 'utf8');
899
+ }
748
900
  }
749
901
  });
750
902
  return result.exitCode;
@@ -1454,6 +1606,20 @@ function formatPersonaShow(spec, source, tiers, tierNote) {
1454
1606
  lines.push(` deny: ${perms.deny.join(', ')}`);
1455
1607
  }
1456
1608
  lines.push('');
1609
+ lines.push('MOUNT');
1610
+ const mount = spec.mount;
1611
+ if (!mount || (!mount.ignoredPatterns?.length && !mount.readonlyPatterns?.length)) {
1612
+ lines.push(' (none)');
1613
+ }
1614
+ else {
1615
+ if (mount.ignoredPatterns?.length) {
1616
+ lines.push(` ignored: ${mount.ignoredPatterns.join(', ')}`);
1617
+ }
1618
+ if (mount.readonlyPatterns?.length) {
1619
+ lines.push(` readonly: ${mount.readonlyPatterns.join(', ')}`);
1620
+ }
1621
+ }
1622
+ lines.push('');
1457
1623
  lines.push('ENV');
1458
1624
  const envKeys = Object.keys(spec.env ?? {});
1459
1625
  if (envKeys.length === 0) {