{"record":{"id":"1f8b691beab89a7b","repo":"paperclipai/paperclip","slug":"refusing-to-write-service-definition-in-directory","errorCode":null,"errorMessage":"Refusing to write service definition in directory not owned by the current user: ${directoryPath}.","messagePattern":"Refusing to write service definition in directory not owned by the current user: (.+?)\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"cli/src/services/service-manager.ts","lineNumber":144,"sourceCode":"  </dict>\n  <key>RunAtLoad</key><true/>\n  <key>KeepAlive</key><true/>\n  <key>ThrottleInterval</key><integer>5</integer>\n  <key>ExitTimeOut</key><integer>300</integer>\n  <key>StandardOutPath</key><string>${escapeXml(input.stdoutPath)}</string>\n  <key>StandardErrorPath</key><string>${escapeXml(input.stderrPath)}</string>\n</dict>\n</plist>\n`;\n}\n\nasync function writeIfChanged(filePath: string, contents: string): Promise<boolean> {\n  const directoryPath = path.dirname(filePath);\n  await fs.mkdir(directoryPath, { recursive: true, mode: 0o700 });\n  const directoryStat = await fs.lstat(directoryPath);\n  if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink()) throw new Error(`Refusing to write service definition through unsafe directory ${directoryPath}.`);\n  const currentUid = process.getuid?.();\n  if (currentUid !== undefined && directoryStat.uid !== currentUid) throw new Error(`Refusing to write service definition in directory not owned by the current user: ${directoryPath}.`);\n  try {\n    const stat = await fs.lstat(filePath);\n    if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink > 1) throw new Error(`Refusing to replace unsafe service definition ${filePath}.`);\n    if (currentUid !== undefined && stat.uid !== currentUid) throw new Error(`Refusing to replace service definition not owned by the current user: ${filePath}.`);\n    if (await fs.readFile(filePath, \"utf8\") === contents) return false;\n  } catch (error) {\n    if ((error as NodeJS.ErrnoException).code !== \"ENOENT\") throw error;\n  }\n  const temporaryPath = path.join(directoryPath, `.${path.basename(filePath)}.tmp-${process.pid}-${Date.now()}`);\n  try {\n    await fs.writeFile(temporaryPath, contents, { encoding: \"utf8\", mode: 0o644, flag: \"wx\" });\n    await fs.rename(temporaryPath, filePath);\n  } finally {\n    await fs.rm(temporaryPath, { force: true });\n  }\n  return true;\n}\n","sourceCodeStart":126,"sourceCodeEnd":162,"githubUrl":"https://github.com/paperclipai/paperclip/blob/67001ec6eb96ae601aa27bc91d9b2415d665334a/cli/src/services/service-manager.ts#L126-L162","documentation":"Thrown by writeIfChanged when the service definition directory's owner UID does not match the current process's UID (process.getuid()). Writing a systemd/launchd service definition into a directory owned by another user could allow privilege escalation if that user controls the directory contents. This check is skipped on platforms where process.getuid is undefined (e.g., Windows).","triggerScenarios":"Calling install/start/restart on a service manager when ~/.config/systemd/user/ or ~/Library/LaunchAgents/ is owned by a different user (e.g., root or another account). This commonly occurs when running as a different user than the one that created the directory, or after a user migration.","commonSituations":"Running paperclipai service commands with sudo (which may set ownership to root). The service directory was created by a different user account. Home directory ownership was changed (e.g., chown -R root ~). Running inside a container where UID mapping differs from the host.","solutions":["Check ownership: 'ls -la ~/.config/systemd/user/' or 'ls -la ~/Library/LaunchAgents/' and compare the owner UID to your current UID ('id -u').","Fix ownership: 'chown -R $(id -u):$(id -g) ~/.config/systemd/user/'.","If you previously ran with sudo, re-run without sudo, or fix ownership of the affected directories.","Verify you are running as the correct user account for this Paperclip instance."],"exampleFix":"// before: directory owned by root\n// ls -la ~/.config/systemd/user/ -> owner: root\n\n// after: fix ownership to current user\n// sudo chown -R $(id -u):$(id -g) ~/.config/systemd/user/\n// chmod 700 ~/.config/systemd/user/","handlingStrategy":"validation","validationCode":"import fs from 'node:fs';\nimport path from 'node:path';\n\nfunction isDirOwnedByCurrentUser(dirPath: string): boolean {\n  const currentUid = process.getuid?.();\n  if (currentUid === undefined) return true; // skip on platforms without getuid\n  try {\n    const stat = fs.lstatSync(dirPath);\n    return stat.isDirectory() && !stat.isSymbolicLink() && stat.uid === currentUid;\n  } catch {\n    return false;\n  }\n}\n\n// Call before service install:\nconst serviceDir = path.dirname(definitionPath);\nif (!isDirOwnedByCurrentUser(serviceDir)) {\n  throw new Error(`Run 'chown -R $(id -u):$(id -g) ${serviceDir}' and retry.`);\n}","typeGuard":null,"tryCatchPattern":"try {\n  await manager.install({ startNow: true, startOnLogin: true });\n} catch (error) {\n  if (error instanceof Error && error.message.includes('not owned by the current user')) {\n    console.error('Fix directory ownership: sudo chown -R $(id -u):$(id -g)', path.dirname(manager.definitionPath));\n  }\n  throw error;\n}","preventionTips":["Never run 'paperclipai service install' with sudo—it can create root-owned service directories.","If directories were created by root, fix ownership with 'sudo chown -R $(id -u):$(id -g) ~/.config/systemd/user/'.","In containers, ensure the runtime UID matches the UID that owns the service directories."],"tags":["security","service-manager","ownership","systemd","launchd","cli"],"backgroundTag":null,"analyzedSha":"67001ec6eb96ae601aa27bc91d9b2415d665334a","analyzedAt":"2026-08-12T12:05:45.408Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}