paperclipai/paperclip · error · Error
Refusing to replace unsafe service definition ${filePath}.
Error message
Refusing to replace unsafe service definition ${filePath}. What it means
Thrown by writeIfChanged when the existing service definition file (e.g., paperclipai.service or the launchd plist) is not a regular file, is a symbolic link, or has a hard link count greater than 1. This triple check (regular-file + not-symlink + nlink<=1) prevents replacing an attacker-controlled symlink or a multiply-linked file that could cause the atomic rename to affect other paths.
Source
Thrown at cli/src/services/service-manager.ts:147
<key>ThrottleInterval</key><integer>5</integer>
<key>ExitTimeOut</key><integer>300</integer>
<key>StandardOutPath</key><string>${escapeXml(input.stdoutPath)}</string>
<key>StandardErrorPath</key><string>${escapeXml(input.stderrPath)}</string>
</dict>
</plist>
`;
}
async function writeIfChanged(filePath: string, contents: string): Promise<boolean> {
const directoryPath = path.dirname(filePath);
await fs.mkdir(directoryPath, { recursive: true, mode: 0o700 });
const directoryStat = await fs.lstat(directoryPath);
if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink()) throw new Error(`Refusing to write service definition through unsafe directory ${directoryPath}.`);
const currentUid = process.getuid?.();
if (currentUid !== undefined && directoryStat.uid !== currentUid) throw new Error(`Refusing to write service definition in directory not owned by the current user: ${directoryPath}.`);
try {
const stat = await fs.lstat(filePath);
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink > 1) throw new Error(`Refusing to replace unsafe service definition ${filePath}.`);
if (currentUid !== undefined && stat.uid !== currentUid) throw new Error(`Refusing to replace service definition not owned by the current user: ${filePath}.`);
if (await fs.readFile(filePath, "utf8") === contents) return false;
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
}
const temporaryPath = path.join(directoryPath, `.${path.basename(filePath)}.tmp-${process.pid}-${Date.now()}`);
try {
await fs.writeFile(temporaryPath, contents, { encoding: "utf8", mode: 0o644, flag: "wx" });
await fs.rename(temporaryPath, filePath);
} finally {
await fs.rm(temporaryPath, { force: true });
}
return true;
}
export class SystemdServiceManager implements ServiceManager {
readonly platform = "systemd" as const;
readonly serviceName: string;View on GitHub (pinned to 67001ec6eb)
Solutions
- Inspect the service definition file: 'ls -la ~/.config/systemd/user/paperclipai.service' (systemd) or the plist path (launchd).
- If it is a symlink, remove it: 'rm ~/.config/systemd/user/paperclipai.service'.
- If it is hard-linked (nlink > 1), remove the extra links or delete the file so the installer can recreate it.
- Retry the service install command.
Example fix
// before: service file is a symlink // ls -la ~/.config/systemd/user/paperclipai.service -> lrwxrwxrwx // after: remove symlink // rm ~/.config/systemd/user/paperclipai.service // re-run: paperclipai service install
Defensive patterns
Strategy: validation
Validate before calling
import fs from 'node:fs';
function isSafeServiceDefinition(filePath: string): boolean {
try {
const stat = fs.lstatSync(filePath);
return stat.isFile() && !stat.isSymbolicLink() && stat.nlink <= 1;
} catch (error) {
return (error as NodeJS.ErrnoException).code === 'ENOENT';
}
}
// Call before service install:
if (!isSafeServiceDefinition(manager.definitionPath)) {
console.warn('Service definition is unsafe (symlink/hardlinked); removing before reinstall.');
fs.rmSync(manager.definitionPath, { force: true });
} Try / catch
try {
await manager.install({ startNow: true, startOnLogin: true });
} catch (error) {
if (error instanceof Error && error.message.includes('unsafe service definition')) {
// Remove the unsafe file and retry
await fs.rm(manager.definitionPath, { force: true });
await manager.install({ startNow: true, startOnLogin: true });
} else {
throw error;
}
} Prevention
- Do not symlink or hard-link service definition files.
- If a dotfile manager manages service files, exclude paperclip service files from its management.
- Run 'ls -la' on the service definition path before install to verify it is a regular file or does not exist.
When it happens
Trigger: Calling install/start/restart on a service manager when the service definition file already exists as a symlink, a non-regular file, or a hard-linked file.
Common situations: A symlink attack placed a symlink at the service definition path. A dotfile manager symlinks the service file. The service file was manually hard-linked to another location for backup.
Related errors
- Refusing to write service definition through unsafe director
- Refusing to write service definition in directory not owned
- Systemd service values must not contain line breaks
- Refusing to activate payload that resolves outside ${paths.i
- Refusing to prune unsafe install-store path ${sourceRoot}.
AI-assisted analysis of paperclipai/paperclip@67001ec6eb (2026-08-12).
Data as JSON: /api/errors/fb24c5ba91ea0570.
Report an issue: GitHub.