Visitar URL original
Allow updating symlinked lockfiles in trusted workspaces · Issue #17222 · pnpm/pnpm · GitHub
Skip to content

Allow updating symlinked lockfiles in trusted workspaces #17222

Closed

Description

@zaverden

Verify latest release

  • I verified that the issue exists in the latest pnpm release

pnpm version

12.15.0, 11.28.7

Which area(s) of pnpm are affected? (leave empty if unsure)

Lockfile

Link to the code that reproduces this issue or a replay of the bug

No response

Reproduction steps

Save the following as repro.mjs and run:

node repro.mjs /absolute/path/to/pnpm

The script creates isolated temporary directories and configuration. It first runs an unchanged installation through a symlink, then adds a local dependency and attempts to update the lockfile. trustLockfile: true is enabled throughout, but currently does not allow this write. Finally, it repeats the update with a regular file as a control.

import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { spawnSync } from 'node:child_process';

const pnpm = process.argv[2]; // Pass an absolute path to pnpm.
assert(pnpm, 'Usage: node repro.mjs /absolute/path/to/pnpm');
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'pnpm-symlink-lockfile-'));
const source = path.join(root, 'source');
const work = path.join(root, 'work');
for (const dir of [source, work]) fs.mkdirSync(dir);
const write = (dir, name, value) => fs.writeFileSync(path.join(dir, name),
  typeof value === 'string' ? value : JSON.stringify(value));
const env = Object.fromEntries(Object.entries(process.env)
  .filter(([key]) => !/^(npm|pnpm)_config_/i.test(key)));
Object.assign(env, {
  HOME: path.join(root, 'home'), XDG_CONFIG_HOME: path.join(root, 'config'),
  XDG_CACHE_HOME: path.join(root, 'cache'), XDG_DATA_HOME: path.join(root, 'data'),
  XDG_STATE_HOME: path.join(root, 'state'), CI: '', NO_COLOR: '1',
  npm_config_userconfig: path.join(root, 'empty.npmrc'),
});
write(root, 'empty.npmrc', '');
const args = ['install', '--lockfile-only', '--offline', '--no-frozen-lockfile', '--ignore-scripts'];
const run = (cwd) => {
  const result = spawnSync(pnpm, args, { cwd, env, encoding: 'utf8', timeout: 30000 });
  if (result.error) throw result.error;
  console.log(`pnpm install in ${cwd}: exit ${result.status}`);
  console.log(result.stdout + result.stderr);
  return result;
};
write(source, 'package.json', { name: 'app', version: '1.0.0', private: true });
write(source, 'pnpm-workspace.yaml', 'packages: []\ntrustLockfile: true\n');
assert.equal(run(source).status, 0);
for (const name of ['package.json', 'pnpm-workspace.yaml']) {
  fs.copyFileSync(path.join(source, name), path.join(work, name));
}
const sourceLock = path.join(source, 'pnpm-lock.yaml');
const workLock = path.join(work, 'pnpm-lock.yaml');
fs.symlinkSync('../source/pnpm-lock.yaml', workLock);
assert.equal(run(work).status, 0); // Reading an unchanged symlinked lockfile works.
fs.mkdirSync(path.join(work, 'dep'));
write(path.join(work, 'dep'), 'package.json', { name: 'dep', version: '1.0.0' });
write(work, 'package.json', {
  name: 'app', version: '1.0.0', private: true, dependencies: { dep: 'file:./dep' },
});
const before = fs.readFileSync(sourceLock, 'utf8');
const linked = run(work); // Fails despite trustLockfile: true.
assert.notEqual(linked.status, 0);
assert.match(linked.stderr, /Refusing to write symlinked lockfile/);
assert(fs.lstatSync(workLock).isSymbolicLink());
assert.equal(fs.readFileSync(sourceLock, 'utf8'), before);
fs.unlinkSync(workLock);
fs.copyFileSync(sourceLock, workLock);
assert.equal(run(work).status, 0); // Regular-file control succeeds.
assert.notEqual(fs.readFileSync(workLock, 'utf8'), before);
console.log('Reproduced. Temporary project:', root);

Observed on 12.15.0:

  1. Initial lockfile generation: exit 0.
  2. Unchanged install with work/pnpm-lock.yaml -> ../source/pnpm-lock.yaml: exit 0. Here “unchanged” means that the lockfile content does not need to change: pnpm reads the existing lockfile through the symlink and skips writing because the resulting content is identical. The lockfile is therefore only read in this step; this does not demonstrate support for writing through the symlink. It also does not mean that an install cannot create or update other files, such as node_modules.
  3. Install after adding dep: file:./dep, keeping that symlink: exit 1.
  4. The identical dependency update with a regular lockfile: exit 0.

The failing command reports:

ERR_PNPM_LOCKFILE_WRITE_FILE
Failed to write lockfile content: Refusing to write symlinked lockfile at .../work/pnpm-lock.yaml

The symlink is retained, but the source lockfile is not updated.

Describe the Bug

This is a follow-up to pnpm/pnpm#13073, addressed by pnpm/pnpm#13075. That change restored reading symlinked lockfiles and allowed installs when the lockfile content does not change. Our remaining case requires writing changed content through the symlink.

We encountered this restriction while upgrading from pnpm 10.33.4, where our workflow could update the lockfile through a symlink. We also encounter the write restriction in pnpm 11.28.7. Supporting this workflow in pnpm 12 would be sufficient for our migration; we do not need the change to be backported to pnpm 11.

Our build and development system prepares a working directory for each project and runs pnpm there. In our scenario, both package.json and pnpm-lock.yaml in the working directory are symlinks to the corresponding files in source_dir. The original source manifest and lockfile remain authoritative:

source/project/package.json                   # tracked source manifest
source/project/pnpm-lock.yaml                  # tracked source lockfile
working/project/package.json -> ../../source/project/package.json
working/project/pnpm-workspace.yaml            # generated configuration
working/project/pnpm-lock.yaml -> ../../source/project/pnpm-lock.yaml

When a developer changes dependencies and regenerates the lockfile, pnpm must update the source file through this link. Reading the lockfile and skipping identical writes is sufficient for an unchanged install, but does not cover this update workflow. Replacing the symlink with a regular file only updates the working copy, leaving the authoritative source lockfile stale. We need the link to remain intact and its target to receive the new content.

In pnpm 12.15.0, save_value_to_path skips identical content but calls the symlink check before a changed write. The same path still exists on the inspected main commit f0982d4f1124e66059b893fb18e75bf6bd3d7a4e. This report requests a supported opt-in for the remaining workflow; it does not claim that the read/no-change fix regressed.

Expected Behavior

Provide an explicit way to update a symlinked lockfile in a trusted working directory. Either of these alternatives would support our system:

  1. Allow symlinked lockfile writes when trustLockfile: true. Extend the existing setting to permit resolving the link and atomically updating its final target. The reproduction already enables this setting and demonstrates the currently missing behavior.

    trustLockfile: true
  2. Add a dedicated allowSymlinkedLockfile setting, defaulting to false, so this behavior can be enabled independently of trustLockfile.

    allowSymlinkedLockfile: true

    This is a proposed new option, not an existing workaround.

With the chosen opt-in enabled, resolve relative or absolute symlinks (including chains), write atomically next to the final target, and preserve the symlink entries and the existing target's file permissions. Cycles should still fail, and unchanged content should still avoid a rewrite.

Which Node.js version are you using?

26.12.0

Which operating systems have you used?

  • macOS
  • Windows
  • Linux

If your OS is a Linux based, which one it is? (Include the version if relevant)

No response

Contributing a fix

  • I am working on a fix

Activity

  1. added
    area: monorepoEverything related to the pnpm workspace feature
    area: supply chain securityIssues related to minimumReleaseAge, blockExoticSubdeps, build script safety, and trust policies.
    on Oct 22, 2026
  2. added theissue type on Oct 22, 2026
  3. self-assigned this
    on Oct 22, 2026
  4. zkochan commented on Oct 22, 2026

    @zkochan
    Member

    A draft PR is open: #17233.

    It takes your second option: a dedicated allowSymlinkedLockfile setting in pnpm v12 (default false), rather than widening trustLockfile, which is about skipping supply-chain verification. With the setting enabled, pnpm resolves the symlink chain (relative or absolute), atomically replaces the final target, keeps the links and the target's file mode, still fails on a cycle, and still skips an unchanged lockfile. It covers both the main lockfile and the env document (config dependencies / packageManager pin). It is not backported to pnpm v11.


    Written by an agent (Claude Code, claude-opus-5-5).

  5. zkochan commented on Oct 22, 2026

    @zkochan
    Member

    Update on #17233: allowSymlinkedLockfile can't be set in pnpm-workspace.yaml after all. The repository commits the symlink, so if it could also allow the write, a cloned repository could overwrite any file you can write. Enable it in your build environment instead, with PNPM_CONFIG_ALLOW_SYMLINKED_LOCKFILE=true or in the global config.yaml.


    Written by an agent (Claude Code, claude-opus-5-5).

  6. zkochan commented on Oct 25, 2026

    @zkochan
    Member

    An update to #17233: allowSymlinkedLockfile is now a list of directories, not true/false. pnpm writes through the symlink only if the file it finally points to is inside one of the listed directories. A true in the global config would have applied to every project on the machine, so any repository you clone could have committed a pnpm-lock.yaml that links to a file such as ~/.bashrc.

    For your setup, list the source tree the links point into:

    # global config.yaml
    allowSymlinkedLockfile:
      - /path/to/source-tree

    or PNPM_CONFIG_ALLOW_SYMLINKED_LOCKFILE='["/path/to/source-tree"]' in the build environment. Entries must be absolute or start with ~/. As before, pnpm ignores the setting in pnpm-workspace.yaml.


    Written by an agent (Claude Code, claude-opus-5-5).

  7. added 2 commits that reference this issue on Oct 25, 2026
    53d3440
    37acbfb
  8. zkochan commented on Oct 25, 2026

    @zkochan
    Member

    Update on #17233: the setting is renamed to symlinkedLockfileTargetDirs, since it is a list of directories rather than a switch. It works as described in my earlier comment:

    # global config.yaml
    symlinkedLockfileTargetDirs:
      - ~/build/src

    or PNPM_CONFIG_SYMLINKED_LOCKFILE_TARGET_DIRS='["/path/to/src"]'.


    Written by an agent (Claude Code, claude-opus-5-5).

  9. added a commit that references this issue on Oct 25, 2026
    d6cb3e8
  10. added a commit that references this issue on Nov 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area: lockfilearea: monorepoEverything related to the pnpm workspace featurearea: supply chain securityIssues related to minimumReleaseAge, blockExoticSubdeps, build script safety, and trust policies.

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions