feat(auth): share canonical Claude memory

Refs #1688
This commit is contained in:
Tam Nhu Tran committed 2026-08-08 21:17:32 -04:00
1 parent 7fb56d62e1
commit 6841025bb4
14 files changed
+1011 -70

No files matched your search

+10 -10
View File
@@ -1,10 +1,10 @@
{
"scope": "src/**/*.{ts,tsx,js,jsx,mjs,cjs}",
"syncFs": {
"totalOccurrences": 2427,
"filesAffected": 258,
"hotpathOccurrences": 1142,
"hotpathFilesAffected": 152,
"totalOccurrences": 2446,
"filesAffected": 260,
"hotpathOccurrences": 1161,
"hotpathFilesAffected": 154,
"topHotpathFiles": [
{
"file": "src/management/shared-manager/diverged-file-adopter.ts",
@@ -498,10 +498,10 @@
"maintainability": {
"typedErrors": {
"totalThrows": 452,
"typedThrows": 80,
"plainThrows": 312,
"typedThrows": 81,
"plainThrows": 311,
"otherThrows": 60,
"adoptionRatio": 0.177,
"adoptionRatio": 0.1792,
"topSubdomainsByThrows": [
{
"subdomain": "web-server",
@@ -579,8 +579,8 @@
},
"loggerCoverage": {
"filesWithCreateLogger": 65,
"totalSourceFiles": 759,
"coverageRatio": 0.0856,
"totalSourceFiles": 761,
"coverageRatio": 0.0854,
"subdomainsWithZeroCreateLogger": [
"api",
"bin",
@@ -616,7 +616,7 @@
},
{
"subdomain": "management",
"count": 33,
"count": 35,
"withLogger": 1
},
{
+6 -6
View File
@@ -6,10 +6,10 @@ Scope: `src/**/*.{ts,tsx,js,jsx,mjs,cjs}`
| Metric | Value |
|---|---:|
| Sync fs occurrences (all) | 2427 |
| Sync fs files affected (all) | 258 |
| Sync fs occurrences (runtime hotpaths) | 1142 |
| Sync fs files affected (runtime hotpaths) | 152 |
| Sync fs occurrences (all) | 2446 |
| Sync fs files affected (all) | 260 |
| Sync fs occurrences (runtime hotpaths) | 1161 |
| Sync fs files affected (runtime hotpaths) | 154 |
| Legacy shim markers | 458 |
| Legacy shim files affected | 173 |
@@ -56,11 +56,11 @@ Scope: `src/**/*.{ts,tsx,js,jsx,mjs,cjs}`
| Metric | Value |
|---|---:|
| typed-error adoption (typed/total throws) | 17.7% (80/452) |
| typed-error adoption (typed/total throws) | 17.9% (81/452) |
| typed-error adoption (P4 locked subdomains) | 93.3% (28/30), target 40% |
| hotpath console.error/warn occurrences | 266 (571 total, 305 CLI-UX exempt) |
| hotpath console.error/warn files | 82 |
| files with createLogger | 65/759 |
| files with createLogger | 65/761 |
| subdomains with zero createLogger | 15 (api, bin, channels, cliproxy, cliproxy/accounts, cliproxy/ai-providers, cliproxy/binary, cliproxy/config, cliproxy/management, cliproxy/sync, cliproxy/types, config, dispatcher, shared, types) |
| files > 400 LOC | 91 |
| files > 600 LOC | 42 |
+3 -3
View File
@@ -142,7 +142,7 @@ class AuthCommands {
` ${color('--deeper-continuity', 'command')} Advanced shared mode: sync additional continuity artifacts`
);
console.log(
` ${color('--bare', 'command')} Create clean profile without shared symlinks (no CK/commands/skills)`
` ${color('--bare', 'command')} Create clean profile without shared settings/instructions/resources`
);
console.log(
` ${color('--mode <mode>', 'command')} Shared resource mode for resources: shared|profile-local`
@@ -169,10 +169,10 @@ class AuthCommands {
);
console.log(` Account logins, tokens, and .anthropic stay isolated for every profile.`);
console.log(
` Non-bare account profiles share basic ${color('settings.json', 'path')} with ${color('~/.claude/settings.json', 'path')}; ${color('ccs auth show <profile>', 'command')} shows the link state.`
` Non-bare account profiles share ${color('settings.json', 'path')} and ${color('CLAUDE.md', 'path')} from ${color('~/.claude/', 'path')}; ${color('ccs auth show <profile>', 'command')} shows resource mode and settings link state.`
);
console.log(
` Shared Resources control plugins/commands/skills/agents/settings.json; History Sync controls project/session continuity only.`
` Shared Resources control plugins/commands/skills/agents/settings.json/CLAUDE.md; History Sync controls project/session continuity only.`
);
console.log(
` History sync is opt-in: both accounts need shared mode and the same ${color('context_group', 'path')}.`
+2 -2
View File
@@ -27,9 +27,9 @@ export interface AccountConfig {
context_group?: string;
/** Shared continuity depth when context_mode='shared' */
continuity_mode?: 'standard' | 'deeper';
/** Account-level shared resource behavior for plugins, commands, skills, agents, and settings.json */
/** Account-level shared resource behavior for plugins, commands, skills, agents, settings.json, and CLAUDE.md */
shared_resource_mode?: 'shared' | 'profile-local';
/** Bare profile: no shared symlinks (commands, skills, agents, settings.json) */
/** Bare profile: no shared symlinks (commands, skills, agents, settings.json, CLAUDE.md) */
bare?: boolean;
}
+111 -5
View File
@@ -5,6 +5,116 @@ export function isAccountInstanceName(name: string): boolean {
return !name.startsWith('.');
}
export function normalizeAccountInstanceName(name: string): string {
return name.replace(/[^a-zA-Z0-9_-]/g, '-').toLowerCase();
}
function unsafeInstancePath(targetPath: string): never {
throw Object.assign(new TypeError(`Unsafe account instance path: ${targetPath}`), {
code: 'EINVAL',
});
}
interface DirectoryIdentity {
device: number;
inode: number;
realPath: string;
}
function readDirectoryIdentity(directoryPath: string): DirectoryIdentity {
const stats = fs.lstatSync(directoryPath);
if (stats.isSymbolicLink() || !stats.isDirectory()) unsafeInstancePath(directoryPath);
return {
device: stats.dev,
inode: stats.ino,
realPath: fs.realpathSync.native(directoryPath),
};
}
function identitiesMatch(left: DirectoryIdentity, right: DirectoryIdentity): boolean {
return (
left.device === right.device && left.inode === right.inode && left.realPath === right.realPath
);
}
function createDirectoryUnderStableParent(directoryPath: string): void {
const parentPath = path.dirname(directoryPath);
const parentBefore = readDirectoryIdentity(parentPath);
try {
fs.mkdirSync(directoryPath, { mode: 0o700 });
readDirectoryIdentity(directoryPath);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
}
let parentAfter: DirectoryIdentity;
try {
parentAfter = readDirectoryIdentity(parentPath);
} catch {
unsafeInstancePath(parentPath);
}
if (!identitiesMatch(parentBefore, parentAfter)) {
unsafeInstancePath(parentPath);
}
readDirectoryIdentity(directoryPath);
}
function isPathWithinDirectory(candidatePath: string, directoryPath: string): boolean {
const relative = path.relative(directoryPath, candidatePath);
return relative.length > 0 && relative !== '..' && !relative.startsWith(`..${path.sep}`);
}
/** Reject symlink roots/entries and require the real instance path to stay under the real root. */
export function isSafeAccountInstancePath(instancesDir: string, instancePath: string): boolean {
try {
const rootStats = fs.lstatSync(instancesDir);
if (rootStats.isSymbolicLink() || !rootStats.isDirectory()) return false;
const instanceStats = fs.lstatSync(instancePath);
if (instanceStats.isSymbolicLink() || !instanceStats.isDirectory()) return false;
const realRoot = fs.realpathSync.native(instancesDir);
const realInstance = fs.realpathSync.native(instancePath);
return isPathWithinDirectory(realInstance, realRoot);
} catch {
return false;
}
}
/** Create a missing managed instances root without traversing symlink parents. */
export function ensureSafeAccountInstancesDirectory(instancesDir: string): void {
const missingPaths: string[] = [];
let currentPath = instancesDir;
while (true) {
try {
const stats = fs.lstatSync(currentPath);
if (stats.isSymbolicLink() || !stats.isDirectory()) unsafeInstancePath(currentPath);
break;
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
missingPaths.push(currentPath);
const parentPath = path.dirname(currentPath);
if (parentPath === currentPath) unsafeInstancePath(instancesDir);
currentPath = parentPath;
}
}
for (const directoryPath of missingPaths.reverse()) {
createDirectoryUnderStableParent(directoryPath);
}
const rootStats = fs.lstatSync(instancesDir);
if (rootStats.isSymbolicLink() || !rootStats.isDirectory()) unsafeInstancePath(instancesDir);
}
export function assertSafeAccountInstancePath(instancesDir: string, instancePath: string): void {
if (!isSafeAccountInstancePath(instancesDir, instancePath)) unsafeInstancePath(instancePath);
}
export function listAccountInstanceNames(instancesDir: string): string[] {
if (!fs.existsSync(instancesDir)) {
return [];
@@ -15,11 +125,7 @@ export function listAccountInstanceNames(instancesDir: string): string[] {
return false;
}
try {
return fs.statSync(path.join(instancesDir, name)).isDirectory();
} catch {
return false;
}
return isSafeAccountInstancePath(instancesDir, path.join(instancesDir, name));
});
}
+33 -32
View File
@@ -15,7 +15,13 @@ import type { AccountContextPolicy } from '../auth/account-context';
import { getCcsHome } from '../utils/config-manager';
import { createLogger } from '../services/logging';
import { getCcsDir } from '../config/config-loader-facade';
import { listAccountInstanceNames } from './instance-directory';
import { ProfileError } from '../errors/error-types';
import {
assertSafeAccountInstancePath,
ensureSafeAccountInstancesDirectory,
listAccountInstanceNames,
normalizeAccountInstanceName,
} from './instance-directory';
const logger = createLogger('management:instance-manager');
@@ -23,7 +29,7 @@ const MANAGED_MCP_SERVER_NAMES = new Set(['ccs-websearch', 'ccs-image-analysis',
/** Options for instance creation */
export interface InstanceOptions {
/** Skip shared symlinks (commands, skills, agents, settings.json) */
/** Skip shared symlinks (commands, skills, agents, settings.json, CLAUDE.md) */
bare?: boolean;
}
@@ -52,11 +58,18 @@ class InstanceManager {
options: InstanceOptions = {}
): Promise<string> {
const instancePath = this.getInstancePath(profileName);
const initialInstanceStats = this.getInstanceStats(instancePath);
if (initialInstanceStats) {
assertSafeAccountInstancePath(this.instancesDir, instancePath);
} else {
ensureSafeAccountInstancesDirectory(this.instancesDir);
}
// Serialize context sync operations per profile across processes.
await this.contextSyncLock.withLock(profileName, async () => {
// Lazy initialization
if (!fs.existsSync(instancePath)) {
if (!this.getInstanceStats(instancePath)) {
ensureSafeAccountInstancesDirectory(this.instancesDir);
logger.stage('route', 'instance.init', 'Initializing new profile instance', {
profile: profileName,
bare: options.bare === true,
@@ -64,6 +77,9 @@ class InstanceManager {
this.initializeInstance(profileName, instancePath, options);
}
// Re-check after locking/creation before any operation can mutate the instance.
assertSafeAccountInstancePath(this.instancesDir, instancePath);
// Validate structure (auto-fix missing dirs)
this.validateInstance(instancePath);
@@ -94,7 +110,7 @@ class InstanceManager {
* Get instance path for profile
*/
getInstancePath(profileName: string): string {
const safeName = this.sanitizeName(profileName);
const safeName = normalizeAccountInstanceName(profileName);
return path.join(this.instancesDir, safeName);
}
@@ -108,30 +124,14 @@ class InstanceManager {
): void {
try {
// Create base directory
fs.mkdirSync(instancePath, { recursive: true, mode: 0o700 });
fs.mkdirSync(instancePath, { mode: 0o700 });
// Create Claude-expected subdirectories (profile-specific only)
const subdirs = [
'session-env',
'todos',
'logs',
'file-history',
'shell-snapshots',
'debug',
'.anthropic',
];
subdirs.forEach((dir) => {
const dirPath = path.join(instancePath, dir);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true, mode: 0o700 });
}
});
// Shared links are created during ensureInstance() under the plugin layout lock.
// Expected subdirectories and shared links are created only after the
// caller validates that this new root stayed inside the managed parent.
} catch (error) {
throw new Error(
`Failed to initialize instance for ${profileName}: ${(error as Error).message}`
throw new ProfileError(
`Failed to initialize instance for ${profileName}: ${(error as Error).message}`,
profileName
);
}
}
@@ -268,12 +268,13 @@ class InstanceManager {
}
}
/**
* Sanitize profile name for filesystem
*/
private sanitizeName(name: string): string {
// Replace unsafe characters with dash
return name.replace(/[^a-zA-Z0-9_-]/g, '-').toLowerCase();
private getInstanceStats(instancePath: string): fs.Stats | null {
try {
return fs.lstatSync(instancePath);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
throw error;
}
}
}
@@ -0,0 +1,123 @@
import * as fs from 'fs';
import * as path from 'path';
import { warn } from '../../utils/ui';
import { getLstatSync } from './fs-helpers';
let claimSequence = 0;
export function readRegularFileContent(filePath: string): Buffer | null {
const stats = getLstatSync(filePath);
return stats?.isFile() ? fs.readFileSync(filePath) : null;
}
function preserveClaim(filePath: string, claimPath: string, reason: string): string {
const basePath = `${filePath}.ccs-shared-conflict`;
let sequence = 0;
while (true) {
const recoveryPath = sequence === 0 ? basePath : `${basePath}-${sequence}`;
try {
fs.linkSync(claimPath, recoveryPath);
try {
fs.unlinkSync(claimPath);
} catch {
// Both names preserve the same inode, so leaving the claim is safe.
}
console.log(warn(`${reason}; preserved content at ${recoveryPath}`));
return recoveryPath;
} catch (err) {
const code = (err as NodeJS.ErrnoException).code;
if (code === 'EEXIST') {
sequence++;
continue;
}
try {
fs.copyFileSync(claimPath, recoveryPath, fs.constants.COPYFILE_EXCL);
fs.chmodSync(recoveryPath, fs.statSync(claimPath).mode & 0o777);
fs.unlinkSync(claimPath);
console.log(
warn(`${reason}; copied content to ${recoveryPath} (${code ?? 'link failed'})`)
);
return recoveryPath;
} catch (copyError) {
if ((copyError as NodeJS.ErrnoException).code === 'EEXIST') {
sequence++;
continue;
}
if (!getLstatSync(filePath)) {
try {
fs.renameSync(claimPath, filePath);
} catch {
// The claim remains the only recoverable copy if restoration loses a race.
}
}
console.log(warn(`${reason}; unable to publish recovery, restored original when possible`));
throw err;
}
}
}
}
function interruptedClaimPaths(filePath: string): string[] {
const directory = path.dirname(filePath);
const prefix = `${path.basename(filePath)}.ccs-shared-claim-`;
try {
return fs
.readdirSync(directory)
.filter((entry) => entry.startsWith(prefix))
.map((entry) => path.join(directory, entry))
.filter((entryPath) => fs.lstatSync(entryPath).isFile())
.sort();
} catch (err) {
if ((err as NodeJS.ErrnoException).code === 'ENOENT') return [];
throw err;
}
}
export function recoverInterruptedClaims(filePath: string): void {
const claims = interruptedClaimPaths(filePath);
if (claims.length === 0) return;
if (claims.length === 1 && !getLstatSync(filePath)) {
fs.renameSync(claims[0], filePath);
console.log(warn(`Recovered interrupted shared-file claim at ${filePath}`));
return;
}
for (const claimPath of claims) {
preserveClaim(filePath, claimPath, `Quarantined interrupted shared-file claim for ${filePath}`);
}
}
export function claimCanonicalFirstCopy(
filePath: string,
canonicalContent: Buffer | null,
reason: string
): boolean {
const initialStats = getLstatSync(filePath);
if (!initialStats?.isFile()) return false;
const claimPath = `${filePath}.ccs-shared-claim-${process.pid}-${Date.now()}-${claimSequence++}`;
fs.renameSync(filePath, claimPath);
if (getLstatSync(filePath)) {
preserveClaim(filePath, claimPath, `Concurrent replacement detected at ${filePath}`);
throw Object.assign(new TypeError(`Concurrent replacement detected at ${filePath}`), {
code: 'EEXIST',
});
}
const claimedContent = fs.readFileSync(claimPath);
if (canonicalContent?.equals(claimedContent)) {
fs.unlinkSync(claimPath);
} else {
preserveClaim(filePath, claimPath, reason);
}
if (getLstatSync(filePath)) {
throw Object.assign(new TypeError(`Path reappeared during reconciliation: ${filePath}`), {
code: 'EEXIST',
});
}
return true;
}
@@ -0,0 +1,142 @@
import * as fs from 'fs';
import * as path from 'path';
import { info, warn } from '../../utils/ui';
import ProfileRegistry from '../../auth/profile-registry';
import { isProfileLocalSharedResourceMode } from '../../auth/shared-resource-policy';
import {
isSafeAccountInstancePath,
listAccountInstancePaths,
normalizeAccountInstanceName,
} from '../instance-directory';
import { adoptDivergedFileContent } from './diverged-file-adopter';
import { getLstatSync } from './fs-helpers';
import {
claimCanonicalFirstCopy,
readRegularFileContent,
recoverInterruptedClaims,
} from './canonical-first-file-claims';
interface CanonicalFirstRoots {
claudeDir: string;
sharedDir: string;
instancesDir: string;
}
function candidatePaths(roots: CanonicalFirstRoots, fileName: string): string[] {
const profiles = new ProfileRegistry().getAllProfilesMerged();
const profilesByInstanceName = new Map<string, Array<(typeof profiles)[string]>>();
for (const [profileName, profile] of Object.entries(profiles)) {
const instanceName = normalizeAccountInstanceName(profileName);
const matches = profilesByInstanceName.get(instanceName) ?? [];
matches.push(profile);
profilesByInstanceName.set(instanceName, matches);
}
return [
path.join(roots.sharedDir, fileName),
...listAccountInstancePaths(roots.instancesDir)
.filter((instancePath) => {
const instanceName = normalizeAccountInstanceName(path.basename(instancePath));
const matches = profilesByInstanceName.get(instanceName) ?? [];
if (matches.length > 1) return false;
return matches.length === 0 || !isProfileLocalSharedResourceMode(matches[0]);
})
.map((instancePath) => path.join(instancePath, fileName)),
];
}
function reconcileCandidatesAgainstCanonical(
canonicalPath: string,
candidates: readonly string[]
): void {
let canonicalContent: Buffer | null = null;
try {
canonicalContent = fs.readFileSync(canonicalPath);
} catch {
// A present but unreadable or dangling canonical path still owns precedence.
}
for (const candidatePath of candidates) {
claimCanonicalFirstCopy(
candidatePath,
canonicalContent,
`Canonical ${path.basename(canonicalPath)} wins over divergent copy at ${candidatePath}`
);
}
}
/**
* Reconcile a canonical-first shared file before any seed or symlink mutation.
* Existing canonical content always wins. With no canonical, one unique byte
* variant is adopted; conflicting variants are all quarantined for recovery.
*/
export function reconcileCanonicalFirstFile(roots: CanonicalFirstRoots, fileName: string): void {
const canonicalPath = path.join(roots.claudeDir, fileName);
const candidates = candidatePaths(roots, fileName);
for (const candidatePath of candidates) {
if (candidatePath !== path.join(roots.sharedDir, fileName)) {
const instancePath = path.dirname(candidatePath);
if (!isSafeAccountInstancePath(roots.instancesDir, instancePath)) continue;
}
recoverInterruptedClaims(candidatePath);
}
if (getLstatSync(canonicalPath)) {
reconcileCandidatesAgainstCanonical(canonicalPath, candidates);
return;
}
const variants: Array<{ path: string; content: Buffer }> = [];
for (const candidatePath of candidates) {
const content = readRegularFileContent(candidatePath);
if (content) variants.push({ path: candidatePath, content });
}
if (variants.length === 0) return;
const uniqueContents: Buffer[] = [];
for (const variant of variants) {
if (!uniqueContents.some((content) => content.equals(variant.content))) {
uniqueContents.push(variant.content);
}
}
if (uniqueContents.length === 1) {
adoptDivergedFileContent(variants[0].path, canonicalPath);
reconcileCandidatesAgainstCanonical(canonicalPath, candidates);
console.log(info(`Adopted the only existing ${fileName} variant as canonical content`));
return;
}
for (const variant of variants) {
claimCanonicalFirstCopy(
variant.path,
null,
`Conflicting ${fileName} variants found while no canonical file exists`
);
}
console.log(
warn(
`No ${fileName} variant was selected; recover one of the .ccs-shared-conflict files explicitly`
)
);
}
/** Preserve a later divergent copy without allowing it to replace canonical content. */
export function preserveCanonicalFirstDivergence(
divergedPath: string,
canonicalPath: string
): boolean {
let canonicalContent: Buffer | null = null;
try {
canonicalContent = fs.readFileSync(canonicalPath);
} catch {
// Preserve the divergence when canonical cannot be compared safely.
}
return claimCanonicalFirstCopy(
divergedPath,
canonicalContent,
`Canonical ${path.basename(canonicalPath)} wins over divergent copy at ${divergedPath}`
);
}
@@ -18,6 +18,7 @@ import * as fs from 'fs';
import * as path from 'path';
import { info, warn } from '../../utils/ui';
import { isSafeAccountInstancePath } from '../instance-directory';
import {
adoptDivergedFileContent,
assertAdoptionPathAbsent,
@@ -41,6 +42,10 @@ import {
ensureSharedPluginLayoutDefaults,
linkInstancePlugins,
} from './plugin-layout-internals';
import {
preserveCanonicalFirstDivergence,
reconcileCanonicalFirstFile,
} from './canonical-first-file-reconciler';
import { SHARED_ITEMS } from './types';
/**
@@ -120,11 +125,15 @@ export function ensureSharedDirectories(roots: LinkerRoots): void {
recoverOrphanedCanonicalClaim(claudePath);
}
if (item.type === 'file' && item.divergencePolicy === 'canonical-first') {
reconcileCanonicalFirstFile(roots, item.name);
}
if (!getLstatSync(claudePath)) {
if (item.type === 'directory') {
fs.mkdirSync(claudePath, { recursive: true, mode: 0o700 });
} else if (item.type === 'file') {
fs.writeFileSync(claudePath, JSON.stringify({}, null, 2), 'utf8');
fs.writeFileSync(claudePath, item.seedContent, 'utf8');
}
}
@@ -149,7 +158,9 @@ export function ensureSharedDirectories(roots: LinkerRoots): void {
// Continue to recreate
}
if (item.type === 'file') {
if (item.type === 'file' && item.divergencePolicy === 'canonical-first') {
removeExisting = !preserveCanonicalFirstDivergence(sharedPath, claudePath);
} else if (item.type === 'file') {
sharedAdoption = adoptDivergedFileContent(sharedPath, claudePath);
removeExisting = sharedAdoption === 'not-claimed';
}
@@ -196,6 +207,11 @@ export function ensureSharedDirectories(roots: LinkerRoots): void {
* Link shared directories into a specific instance path.
*/
export function linkSharedDirectories(roots: LinkerRoots, instancePath: string): void {
if (!isSafeAccountInstancePath(roots.instancesDir, instancePath)) {
throw Object.assign(new TypeError(`Unsafe account instance path: ${instancePath}`), {
code: 'EINVAL',
});
}
ensureSharedDirectories(roots);
const sharedDir = roots.sharedDir;
@@ -210,7 +226,11 @@ export function linkSharedDirectories(roots: LinkerRoots, instancePath: string):
const targetPath = path.join(sharedDir, item.name);
let adoption: ReturnType<typeof adoptDivergedFileContent> = 'not-claimed';
if (item.type === 'file') {
if (item.type === 'file' && item.divergencePolicy === 'canonical-first') {
if (!preserveCanonicalFirstDivergence(linkPath, path.join(roots.claudeDir, item.name))) {
removeExistingPath(linkPath, item.type);
}
} else if (item.type === 'file') {
adoption = adoptDivergedFileContent(linkPath, path.join(roots.claudeDir, item.name));
if (adoption === 'not-claimed') {
removeExistingPath(linkPath, item.type);
+27 -2
View File
@@ -16,6 +16,20 @@ export interface SharedItem {
type: 'directory' | 'file';
}
export interface ManagedSharedDirectoryItem extends SharedItem {
type: 'directory';
}
export interface ManagedSharedFileItem extends SharedItem {
type: 'file';
/** Exact bytes used when no canonical or adoptable source file exists. */
seedContent: string;
/** Controls whether an instance may replace an existing canonical file. */
divergencePolicy: 'newer-wins' | 'canonical-first';
}
export type ManagedSharedItem = ManagedSharedDirectoryItem | ManagedSharedFileItem;
/**
* Default content for a freshly provisioned installed_plugins.json registry.
* Version 2 schema with an empty plugins map.
@@ -36,12 +50,23 @@ export const DEFAULT_INSTALLED_PLUGIN_REGISTRY = JSON.stringify(
* Order matters: consumers rely on a stable iteration order when reconciling
* symlinks, and 'plugins' is special-cased by the linker.
*/
export const SHARED_ITEMS: readonly SharedItem[] = [
export const SHARED_ITEMS: readonly ManagedSharedItem[] = [
{ name: 'commands', type: 'directory' },
{ name: 'skills', type: 'directory' },
{ name: 'agents', type: 'directory' },
{ name: 'plugins', type: 'directory' },
{ name: 'settings.json', type: 'file' },
{
name: 'settings.json',
type: 'file',
seedContent: JSON.stringify({}, null, 2),
divergencePolicy: 'newer-wins',
},
{
name: 'CLAUDE.md',
type: 'file',
seedContent: '',
divergencePolicy: 'canonical-first',
},
];
/**
+2 -2
View File
@@ -121,9 +121,9 @@ export interface ProfileMetadata {
context_group?: string;
/** Shared continuity depth when context_mode='shared' */
continuity_mode?: 'standard' | 'deeper';
/** Account-level shared resource behavior for plugins, commands, skills, agents, and settings.json */
/** Account-level shared resource behavior for plugins, commands, skills, agents, settings.json, and CLAUDE.md */
shared_resource_mode?: 'shared' | 'profile-local';
/** Bare profile: no shared symlinks (commands, skills, agents, settings.json) */
/** Bare profile: no shared symlinks (commands, skills, agents, settings.json, CLAUDE.md) */
bare?: boolean;
}
+166 -5
View File
@@ -284,6 +284,168 @@ describe('InstanceManager MCP sync', () => {
expect(manager.listInstances()).toEqual(['work']);
});
for (const testCase of [
{
label: 'isolated bare',
policy: { mode: 'isolated' as const },
options: { bare: true },
},
{
label: 'shared context',
policy: { mode: 'shared' as const, group: 'review-safety' },
options: {},
},
]) {
it(`rejects an existing symlink instance root before ${testCase.label} mutation`, async () => {
const externalInstance = path.join(tempRoot, `external-${testCase.label.replace(' ', '-')}`);
const instancesDir = path.join(tempRoot, '.ccs', 'instances');
fs.mkdirSync(externalInstance, { recursive: true });
fs.writeFileSync(path.join(externalInstance, 'sentinel.txt'), 'unchanged', 'utf8');
fs.mkdirSync(instancesDir, { recursive: true });
fs.symlinkSync(externalInstance, path.join(instancesDir, 'work'), 'dir');
const manager = new InstanceManager();
await expect(
manager.ensureInstance('work', testCase.policy, testCase.options)
).rejects.toThrow('Unsafe account instance path');
expect(fs.readdirSync(externalInstance)).toEqual(['sentinel.txt']);
expect(fs.readFileSync(path.join(externalInstance, 'sentinel.txt'), 'utf8')).toBe(
'unchanged'
);
expect(fs.readdirSync(instancesDir)).toEqual(['work']);
});
}
it('rejects a symlinked instances parent before creating a new instance or lock', async () => {
const externalInstances = path.join(tempRoot, 'external-instances');
const managedInstances = path.join(tempRoot, '.ccs', 'instances');
fs.mkdirSync(externalInstances, { recursive: true });
fs.mkdirSync(path.dirname(managedInstances), { recursive: true });
fs.symlinkSync(externalInstances, managedInstances, 'dir');
const manager = new InstanceManager();
await expect(
manager.ensureInstance('new-profile', { mode: 'shared', group: 'review-safety' })
).rejects.toThrow('Unsafe account instance path');
expect(fs.readdirSync(externalInstances)).toEqual([]);
});
it('detects parent replacement and leaves only an empty redirected artifact', async () => {
const managedCcsDir = path.join(tempRoot, '.ccs');
const managedInstances = path.join(managedCcsDir, 'instances');
const displacedCcsDir = path.join(tempRoot, 'displaced-ccs');
const externalTarget = path.join(tempRoot, 'parent-race-target');
fs.mkdirSync(managedCcsDir, { recursive: true });
fs.mkdirSync(externalTarget, { recursive: true });
fs.writeFileSync(path.join(externalTarget, 'sentinel.txt'), 'unchanged', 'utf8');
const originalMkdirSync = fs.mkdirSync;
let replacedParent = false;
spyOn(fs, 'mkdirSync').mockImplementation((directoryPath, options) => {
if (directoryPath === managedInstances && !replacedParent) {
replacedParent = true;
fs.renameSync(managedCcsDir, displacedCcsDir);
fs.symlinkSync(externalTarget, managedCcsDir, 'dir');
}
return originalMkdirSync(directoryPath, options);
});
const manager = new InstanceManager();
await expect(
manager.ensureInstance('new-profile', { mode: 'isolated' }, { bare: true })
).rejects.toThrow('Unsafe account instance path');
expect(replacedParent).toBe(true);
expect(fs.readdirSync(externalTarget).sort()).toEqual(['instances', 'sentinel.txt']);
expect(fs.readdirSync(path.join(externalTarget, 'instances'))).toEqual([]);
expect(fs.existsSync(path.join(externalTarget, 'instances', '.locks'))).toBe(false);
expect(fs.existsSync(path.join(externalTarget, 'instances', 'new-profile'))).toBe(false);
expect(fs.readdirSync(displacedCcsDir)).toEqual([]);
});
it('preserves created and unrelated directories when the redirected path is replaced', async () => {
const managedCcsDir = path.join(tempRoot, '.ccs');
const managedInstances = path.join(managedCcsDir, 'instances');
const displacedCcsDir = path.join(tempRoot, 'displaced-ccs-cleanup-race');
const externalTarget = path.join(tempRoot, 'cleanup-race-target');
const createdDirectory = path.join(externalTarget, 'created-instances');
const redirectedInstances = path.join(externalTarget, 'instances');
fs.mkdirSync(managedCcsDir, { recursive: true });
fs.mkdirSync(externalTarget, { recursive: true });
const originalMkdirSync = fs.mkdirSync;
const originalLstatSync = fs.lstatSync;
let replacedParent = false;
let createdIdentityObserved = false;
let substitutedUnrelatedDirectory = false;
spyOn(fs, 'mkdirSync').mockImplementation((directoryPath, options) => {
if (directoryPath === managedInstances && !replacedParent) {
replacedParent = true;
fs.renameSync(managedCcsDir, displacedCcsDir);
fs.symlinkSync(externalTarget, managedCcsDir, 'dir');
}
return originalMkdirSync(directoryPath, options);
});
spyOn(fs, 'lstatSync').mockImplementation((targetPath, options) => {
if (
targetPath === managedCcsDir &&
createdIdentityObserved &&
!substitutedUnrelatedDirectory
) {
substitutedUnrelatedDirectory = true;
fs.renameSync(redirectedInstances, createdDirectory);
originalMkdirSync(redirectedInstances);
}
const stats = originalLstatSync(targetPath, options);
if (targetPath === managedInstances && replacedParent && stats.isDirectory()) {
createdIdentityObserved = true;
}
return stats;
});
const manager = new InstanceManager();
await expect(
manager.ensureInstance('new-profile', { mode: 'isolated' }, { bare: true })
).rejects.toThrow('Unsafe account instance path');
expect(substitutedUnrelatedDirectory).toBe(true);
expect(fs.existsSync(redirectedInstances)).toBe(true);
expect(fs.readdirSync(redirectedInstances)).toEqual([]);
expect(fs.existsSync(createdDirectory)).toBe(true);
expect(fs.readdirSync(createdDirectory)).toEqual([]);
expect(fs.existsSync(path.join(redirectedInstances, '.locks'))).toBe(false);
expect(fs.existsSync(path.join(redirectedInstances, 'new-profile'))).toBe(false);
expect(fs.existsSync(path.join(createdDirectory, '.locks'))).toBe(false);
expect(fs.existsSync(path.join(createdDirectory, 'new-profile'))).toBe(false);
});
it('revalidates a newly created instance root before initializing its contents', async () => {
const externalInstance = path.join(tempRoot, 'post-create-race-target');
fs.mkdirSync(externalInstance, { recursive: true });
fs.writeFileSync(path.join(externalInstance, 'sentinel.txt'), 'unchanged', 'utf8');
const manager = new InstanceManager();
const instancePath = manager.getInstancePath('new-profile');
const originalMkdirSync = fs.mkdirSync;
spyOn(fs, 'mkdirSync').mockImplementation((directoryPath, options) => {
if (directoryPath === instancePath) {
fs.symlinkSync(externalInstance, instancePath, 'dir');
return undefined;
}
return originalMkdirSync(directoryPath, options);
});
await expect(
manager.ensureInstance('new-profile', { mode: 'isolated' }, { bare: true })
).rejects.toThrow('Unsafe account instance path');
expect(fs.readdirSync(externalInstance)).toEqual(['sentinel.txt']);
expect(fs.readFileSync(path.join(externalInstance, 'sentinel.txt'), 'utf8')).toBe('unchanged');
});
it('skips shared symlinks and MCP sync for bare instance creation', async () => {
const linkSharedSpy = spyOn(
SharedManager.prototype,
@@ -491,7 +653,9 @@ describe('InstanceManager MCP sync', () => {
metadata?: Record<string, unknown>;
}
>;
const personalRegistry = readJson(path.join(personalPath, 'plugins', 'known_marketplaces.json')) as Record<
const personalRegistry = readJson(
path.join(personalPath, 'plugins', 'known_marketplaces.json')
) as Record<
string,
{
installLocation?: string;
@@ -552,10 +716,7 @@ describe('InstanceManager MCP sync', () => {
const legacyRegistry = readJson(
path.join(legacyPath, 'plugins', 'known_marketplaces.json')
) as Record<
string,
{ installLocation?: string; label?: string; refreshToken?: string }
>;
) as Record<string, { installLocation?: string; label?: string; refreshToken?: string }>;
expect(legacyRegistry['claude-code-plugins']).toMatchObject({
installLocation: marketplacePath(legacyPath),
label: 'Legacy marketplace',
@@ -86,6 +86,26 @@ describe('account instance directory enumeration', () => {
expect(listAccountInstanceNames(instancesDir())).toEqual(['work']);
});
it('rejects instance entries that symlink outside the managed root', () => {
const externalInstance = path.join(tempRoot, 'external-instance');
fs.mkdirSync(externalInstance, { recursive: true });
fs.mkdirSync(instancesDir(), { recursive: true });
fs.symlinkSync(externalInstance, path.join(instancesDir(), 'escaped'), 'dir');
expect(listAccountInstanceNames(instancesDir())).toEqual([]);
expect(listAccountInstancePaths(instancesDir())).toEqual([]);
});
it('rejects an instances root that is itself a symlink', () => {
const externalInstances = path.join(tempRoot, 'external-instances');
fs.mkdirSync(path.join(externalInstances, 'work'), { recursive: true });
fs.mkdirSync(path.dirname(instancesDir()), { recursive: true });
fs.symlinkSync(externalInstances, instancesDir(), 'dir');
expect(listAccountInstanceNames(instancesDir())).toEqual([]);
expect(listAccountInstancePaths(instancesDir())).toEqual([]);
});
it('keeps ccs doctor settings symlinks healthy when .locks exists', () => {
createValidSettingsLayout();
+343
View File
@@ -0,0 +1,343 @@
import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import SharedManager from '../../src/management/shared-manager';
describe('shared CLAUDE.md memory', () => {
let tempHome = '';
let originalHome: string | undefined;
let originalCcsHome: string | undefined;
let originalCcsDir: string | undefined;
const claudeFile = () => path.join(tempHome, '.claude', 'CLAUDE.md');
const ccsDir = () => path.join(tempHome, '.ccs');
const sharedFile = () => path.join(ccsDir(), 'shared', 'CLAUDE.md');
const instanceDir = (name: string) => path.join(ccsDir(), 'instances', name);
const instanceFile = (name: string) => path.join(instanceDir(name), 'CLAUDE.md');
function writeFile(filePath: string, content: string): void {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, content, 'utf8');
}
function conflictFiles(filePath: string): string[] {
const prefix = `${path.basename(filePath)}.ccs-shared-conflict`;
return fs
.readdirSync(path.dirname(filePath))
.filter((entry) => entry === prefix || entry.startsWith(`${prefix}-`))
.map((entry) => path.join(path.dirname(filePath), entry));
}
function writeLegacyProfiles(
profiles: Record<string, { shared_resource_mode?: string; bare?: boolean }>
): void {
writeFile(
path.join(ccsDir(), 'profiles.json'),
JSON.stringify(
{
version: '2.0.0',
default: null,
profiles: Object.fromEntries(
Object.entries(profiles).map(([name, metadata]) => [
name,
{
type: 'account',
created: '2026-08-08T00:00:00.000Z',
last_used: null,
...metadata,
},
])
),
},
null,
2
)
);
}
function expectLinkTo(filePath: string, targetPath: string): void {
expect(fs.lstatSync(filePath).isSymbolicLink()).toBe(true);
expect(path.resolve(path.dirname(filePath), fs.readlinkSync(filePath))).toBe(targetPath);
}
beforeEach(() => {
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-shared-claude-memory-'));
originalHome = process.env.HOME;
originalCcsHome = process.env.CCS_HOME;
originalCcsDir = process.env.CCS_DIR;
spyOn(os, 'homedir').mockReturnValue(tempHome);
process.env.HOME = tempHome;
process.env.CCS_HOME = tempHome;
delete process.env.CCS_DIR;
});
afterEach(() => {
mock.restore();
if (originalHome === undefined) delete process.env.HOME;
else process.env.HOME = originalHome;
if (originalCcsHome === undefined) delete process.env.CCS_HOME;
else process.env.CCS_HOME = originalCcsHome;
if (originalCcsDir === undefined) delete process.env.CCS_DIR;
else process.env.CCS_DIR = originalCcsDir;
fs.rmSync(tempHome, { recursive: true, force: true });
});
it('seeds missing Markdown as empty text and links it through the shared root', () => {
const manager = new SharedManager();
const workInstance = instanceDir('work');
fs.mkdirSync(workInstance, { recursive: true });
manager.linkSharedDirectories(workInstance);
expect(fs.readFileSync(claudeFile(), 'utf8')).toBe('');
expectLinkTo(sharedFile(), claudeFile());
expectLinkTo(instanceFile('work'), sharedFile());
});
it('preserves canonical instructions and quarantines an older instance variant', () => {
const manager = new SharedManager();
writeFile(claudeFile(), '# Canonical\n');
writeFile(instanceFile('work'), '# Stale local copy\n');
manager.linkSharedDirectories(instanceDir('work'));
expect(fs.readFileSync(claudeFile(), 'utf8')).toBe('# Canonical\n');
expectLinkTo(instanceFile('work'), sharedFile());
const recoveries = conflictFiles(instanceFile('work'));
expect(recoveries).toHaveLength(1);
expect(fs.readFileSync(recoveries[0], 'utf8')).toBe('# Stale local copy\n');
});
it('adopts the only instance variant when the canonical file is absent', () => {
const manager = new SharedManager();
writeFile(instanceFile('work'), '# Adopt me\n');
manager.linkSharedDirectories(instanceDir('work'));
expect(fs.readFileSync(claudeFile(), 'utf8')).toBe('# Adopt me\n');
expectLinkTo(instanceFile('work'), sharedFile());
});
it('never inventories or mutates profile-local CLAUDE.md content', () => {
const manager = new SharedManager();
writeLegacyProfiles({
work: { shared_resource_mode: 'shared' },
sandbox: { shared_resource_mode: 'profile-local', bare: true },
});
writeFile(instanceFile('work'), '# Shared account\n');
writeFile(instanceFile('sandbox'), '# Isolated account\n');
manager.linkSharedDirectories(instanceDir('work'));
expect(fs.readFileSync(claudeFile(), 'utf8')).toBe('# Shared account\n');
expect(fs.readFileSync(instanceFile('sandbox'), 'utf8')).toBe('# Isolated account\n');
expect(conflictFiles(instanceFile('sandbox'))).toEqual([]);
expectLinkTo(instanceFile('work'), sharedFile());
});
it('matches uppercase profile metadata to its normalized instance name', () => {
const manager = new SharedManager();
writeLegacyProfiles({
work: { shared_resource_mode: 'shared' },
Sandbox: { shared_resource_mode: 'profile-local', bare: true },
});
writeFile(instanceFile('work'), '# Shared account\n');
writeFile(instanceFile('sandbox'), '# Uppercase isolated account\n');
manager.linkSharedDirectories(instanceDir('work'));
expect(fs.readFileSync(claudeFile(), 'utf8')).toBe('# Shared account\n');
expect(fs.readFileSync(instanceFile('sandbox'), 'utf8')).toBe('# Uppercase isolated account\n');
expect(conflictFiles(instanceFile('sandbox'))).toEqual([]);
});
it('excludes legacy profile-name normalization collisions from inventory', () => {
const manager = new SharedManager();
writeLegacyProfiles({
work: { shared_resource_mode: 'shared' },
Sandbox: { shared_resource_mode: 'profile-local', bare: true },
sandbox: { shared_resource_mode: 'shared' },
});
writeFile(instanceFile('work'), '# Shared account\n');
writeFile(instanceFile('sandbox'), '# Ambiguous legacy account\n');
manager.linkSharedDirectories(instanceDir('work'));
expect(fs.readFileSync(claudeFile(), 'utf8')).toBe('# Shared account\n');
expect(fs.readFileSync(instanceFile('sandbox'), 'utf8')).toBe('# Ambiguous legacy account\n');
expect(conflictFiles(instanceFile('sandbox'))).toEqual([]);
});
it('quarantines every distinct variant instead of selecting by mtime', () => {
const manager = new SharedManager();
const logSpy = spyOn(console, 'log').mockImplementation(() => {});
writeFile(instanceFile('work'), '# Work\n');
writeFile(instanceFile('personal'), '# Personal\n');
const now = Date.now();
fs.utimesSync(instanceFile('work'), new Date(now - 60_000), new Date(now - 60_000));
fs.utimesSync(instanceFile('personal'), new Date(now), new Date(now));
manager.linkSharedDirectories(instanceDir('work'));
expect(fs.readFileSync(claudeFile(), 'utf8')).toBe('');
expect(fs.readFileSync(conflictFiles(instanceFile('work'))[0], 'utf8')).toBe('# Work\n');
expect(fs.readFileSync(conflictFiles(instanceFile('personal'))[0], 'utf8')).toBe(
'# Personal\n'
);
expect(
logSpy.mock.calls.some(([message]) =>
String(message).includes('No CLAUDE.md variant was selected')
)
).toBe(true);
});
it('is idempotent after canonical-first reconciliation', () => {
const manager = new SharedManager();
writeFile(claudeFile(), '# Canonical\n');
writeFile(instanceFile('work'), '# Diverged\n');
manager.linkSharedDirectories(instanceDir('work'));
const firstRecoveries = conflictFiles(instanceFile('work'));
manager.linkSharedDirectories(instanceDir('work'));
expect(conflictFiles(instanceFile('work'))).toEqual(firstRecoveries);
expect(fs.readFileSync(claudeFile(), 'utf8')).toBe('# Canonical\n');
expectLinkTo(instanceFile('work'), sharedFile());
});
it('never lets a later diverged instance overwrite canonical instructions', () => {
const manager = new SharedManager();
writeFile(claudeFile(), '# Canonical\n');
fs.mkdirSync(instanceDir('work'), { recursive: true });
manager.linkSharedDirectories(instanceDir('work'));
fs.unlinkSync(instanceFile('work'));
writeFile(instanceFile('work'), '# Newer but local\n');
const future = new Date(Date.now() + 60_000);
fs.utimesSync(instanceFile('work'), future, future);
manager.linkSharedDirectories(instanceDir('work'));
expect(fs.readFileSync(claudeFile(), 'utf8')).toBe('# Canonical\n');
expect(fs.readFileSync(conflictFiles(instanceFile('work'))[0], 'utf8')).toBe(
'# Newer but local\n'
);
expectLinkTo(instanceFile('work'), sharedFile());
});
for (const linkErrorCode of ['EPERM', 'EACCES', 'ENOTSUP']) {
it(`copies conflict recovery when hard links fail with ${linkErrorCode}`, () => {
const manager = new SharedManager();
const originalLinkSync = fs.linkSync;
writeFile(claudeFile(), '# Canonical\n');
writeFile(instanceFile('work'), '# Diverged\n');
spyOn(fs, 'linkSync').mockImplementation((existingPath, newPath) => {
if (String(newPath).includes('.ccs-shared-conflict')) {
throw Object.assign(new Error('simulated hard-link failure'), { code: linkErrorCode });
}
return originalLinkSync(existingPath, newPath);
});
manager.linkSharedDirectories(instanceDir('work'));
expect(fs.readFileSync(conflictFiles(instanceFile('work'))[0], 'utf8')).toBe('# Diverged\n');
expectLinkTo(instanceFile('work'), sharedFile());
});
}
it('restores the original divergence when hard-link and copy recovery both fail', () => {
const manager = new SharedManager();
const originalCopyFileSync = fs.copyFileSync;
writeFile(claudeFile(), '# Canonical\n');
writeFile(instanceFile('work'), '# Diverged\n');
spyOn(fs, 'linkSync').mockImplementation(() => {
throw Object.assign(new Error('simulated hard-link failure'), { code: 'EPERM' });
});
spyOn(fs, 'copyFileSync').mockImplementation((source, destination, mode) => {
if (String(destination).includes('.ccs-shared-conflict')) {
throw Object.assign(new Error('simulated copy failure'), { code: 'EACCES' });
}
return originalCopyFileSync(source, destination, mode);
});
expect(() => manager.linkSharedDirectories(instanceDir('work'))).toThrow(
'simulated hard-link failure'
);
expect(fs.readFileSync(instanceFile('work'), 'utf8')).toBe('# Diverged\n');
expect(
fs.readdirSync(instanceDir('work')).some((entry) => entry.includes('.ccs-shared-claim-'))
).toBe(false);
});
it('recovers an interrupted claim before inventory and adoption', () => {
const manager = new SharedManager();
const claimPath = `${instanceFile('work')}.ccs-shared-claim-interrupted`;
writeFile(claimPath, '# Interrupted\n');
manager.linkSharedDirectories(instanceDir('work'));
expect(fs.readFileSync(claudeFile(), 'utf8')).toBe('# Interrupted\n');
expect(fs.existsSync(claimPath)).toBe(false);
expectLinkTo(instanceFile('work'), sharedFile());
});
it('preserves both copies and aborts when a concurrent writer replaces the claimed path', () => {
const manager = new SharedManager();
const originalRenameSync = fs.renameSync;
writeFile(claudeFile(), '# Canonical\n');
writeFile(instanceFile('work'), '# Claimed\n');
spyOn(fs, 'renameSync').mockImplementation((oldPath, newPath) => {
originalRenameSync(oldPath, newPath);
if (oldPath === instanceFile('work') && String(newPath).includes('.ccs-shared-claim-')) {
writeFile(instanceFile('work'), '# Concurrent writer\n');
}
});
expect(() => manager.linkSharedDirectories(instanceDir('work'))).toThrow(
'Concurrent replacement detected'
);
expect(fs.readFileSync(instanceFile('work'), 'utf8')).toBe('# Concurrent writer\n');
expect(fs.readFileSync(conflictFiles(instanceFile('work'))[0], 'utf8')).toBe('# Claimed\n');
expect(fs.readFileSync(claudeFile(), 'utf8')).toBe('# Canonical\n');
});
it('aborts when a late writer recreates the path during recovery publication', () => {
const manager = new SharedManager();
const originalLinkSync = fs.linkSync;
writeFile(claudeFile(), '# Canonical\n');
writeFile(instanceFile('work'), '# Claimed\n');
spyOn(fs, 'linkSync').mockImplementation((existingPath, newPath) => {
const result = originalLinkSync(existingPath, newPath);
if (String(newPath).includes('.ccs-shared-conflict')) {
writeFile(instanceFile('work'), '# Late writer\n');
}
return result;
});
expect(() => manager.linkSharedDirectories(instanceDir('work'))).toThrow(
'Path reappeared during reconciliation'
);
expect(fs.readFileSync(instanceFile('work'), 'utf8')).toBe('# Late writer\n');
expect(fs.readFileSync(conflictFiles(instanceFile('work'))[0], 'utf8')).toBe('# Claimed\n');
});
it('rejects linking through a symlinked instances root without touching its target', () => {
const manager = new SharedManager();
const managedInstances = path.join(ccsDir(), 'instances');
const externalInstances = path.join(tempHome, 'external-instances');
const externalWork = path.join(externalInstances, 'work');
writeFile(path.join(externalWork, 'CLAUDE.md'), '# External\n');
fs.mkdirSync(path.dirname(managedInstances), { recursive: true });
fs.symlinkSync(externalInstances, managedInstances, 'dir');
expect(() => manager.linkSharedDirectories(path.join(managedInstances, 'work'))).toThrow(
'Unsafe account instance path'
);
expect(fs.readFileSync(path.join(externalWork, 'CLAUDE.md'), 'utf8')).toBe('# External\n');
});
});