[GH-ISSUE #7618] [Bug] Concurrent skill installation reports an input-read change with permission advice #4547

Closed
opened 2026-10-31 00:50:40 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Oct 27, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/7618

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Setup, packaging, or upgrade

Observed behavior

I ran six node scripts/rea.mjs setup --yes --skill-client claude_code --json processes against one fresh isolated HOME. One returned ready and installed the skill. Five returned needs_human, code skill_install_failed, with a write-stage message saying the selected input changed while being read and advising a permission check.

The destination was being created by sibling setup processes. The final eight managed files matched the bundled files byte for byte, and no skill staging directory remained. Running the same command sequentially afterward returned ready with no applied actions and no permission change. Two independent six-process rounds produced this same 1/5 split, with every refusal naming SKILL.md.

Reproduction

  1. Start in the built baseline checkout with Node.js v24.22.1 first on PATH. The checkout's existing runtime and bundled skill must already be built. No dependency installation is part of this reproduction.
  2. Run this Python harness from the checkout root. It discards inherited client overrides, uses one fresh home for the six processes, isolates HOME/XDG/npm/temp paths, and puts recording-only npx, npm and bun fixtures first on PATH before setup. It deletes its temporary tree afterward.
python3 -I - <<'PY_REPRO'
import concurrent.futures, json, pathlib, shutil, subprocess, tempfile
repo = pathlib.Path.cwd()
node = str(pathlib.Path(shutil.which('node')).resolve())
with tempfile.TemporaryDirectory() as trial:
    root = pathlib.Path(trial)
    home, fixtures = root / 'home', root / 'bin'
    home.mkdir(); fixtures.mkdir()
    runner = '''#!/bin/sh
printf '%s' "${0##*/}" >> "$FIXTURE_LOG"
for arg in "$@"; do printf ' <%s>' "$arg" >> "$FIXTURE_LOG"; done
printf '\\n' >> "$FIXTURE_LOG"
printf '6.8.0\\n'
'''
    for name in ('npx', 'npm', 'bun'):
        executable = fixtures / name
        executable.write_text(runner); executable.chmod(0o755)
    env = {'HOME': str(home), 'PATH': f'{fixtures}:{pathlib.Path(node).parent}:/usr/bin:/bin',
           'FIXTURE_LOG': str(root / 'runners.log'), 'CI': '1', 'NO_COLOR': '1'}
    for key, name in (('XDG_CONFIG_HOME', 'config'), ('XDG_CACHE_HOME', 'cache'),
                      ('XDG_DATA_HOME', 'data'), ('XDG_STATE_HOME', 'state'),
                      ('TMPDIR', 'tmp'), ('npm_config_prefix', 'npm-global')):
        directory = home / name; directory.mkdir(); env[key] = str(directory)
    for name in ('npx', 'npm', 'bun'):
        assert shutil.which(name, path=env['PATH']) == str(fixtures / name)
    cli = [node, str(repo / 'scripts/rea.mjs')]
    print('REA:', subprocess.check_output(cli + ['--version'], env=env, cwd=home, text=True).strip())
    def run(_):
        result = subprocess.run(cli + ['setup', '--yes', '--skill-client', 'claude_code', '--json'],
                                cwd=home, env=env, capture_output=True, text=True, timeout=90)
        data = json.loads(result.stdout)
        return {'exit': result.returncode, 'status': data['status'], 'code': data.get('code'),
                'remediation': data.get('remediation'), 'appliedActions': data['appliedActions']}
    with concurrent.futures.ThreadPoolExecutor(max_workers=6) as workers:
        results = list(workers.map(run, range(6)))
    print(json.dumps(results, indent=2).replace(str(home.resolve()), '<HOME>'))
    skill = home / '.claude/skills/reverse-engineer-anything'
    files = [p for p in skill.rglob('*') if p.is_file()]
    print('managed files:', len(files))
    print('canonical:', all(p.read_bytes() == (repo / 'skills/reverse-engineer-anything' / p.relative_to(skill)).read_bytes() for p in files))
    print('staging directories:', len(list(home.rglob('.rea-skill-*'))))
    print('sequential retry:', json.dumps(run(None)))
    print('package runners:', (root / 'runners.log').read_text() if (root / 'runners.log').exists() else 'none invoked')
PY_REPRO
  1. Inspect the six result objects, final file checks and sequential retry. In both executed rounds: one ready/exit 0, five needs_human/exit 1 with the diagnostic below, eight canonical files, zero staging directories, and a ready sequential retry with appliedActions: []. Neither round invoked a package runner. The inline harness itself was executed in the second round. This is a scheduling-dependent race; exact counts and named files may vary on another run.

Expected behavior

The installation documentation explicitly says setup refuses a concurrently created destination and advises against overlapping installers. The refusal itself is expected. Its explanation should identify the changed destination or concurrent creation and recommend retrying after the other installer finishes. The repository also requires meaningful failure reasons.

A publication-time destination conflict should not be described as an input changing during a read with generic permission-check advice. The documentation does not promise that every concurrent process succeeds or require a particular skill conflict enum.

Impact and scope

P3: misleading diagnostic wording. The losing caller receives an inaccurate stage explanation and unnecessary permission advice, although the same message also recommends rerunning setup, which worked in both rounds. No corruption, permission failure or unfinished staging cleanup was observed. The successful installation remained complete.

Verified scope is first installation of the same bundled skill by six processes on one macOS host. Different REA versions, existing-file update/backup races and other platforms were not exercised.

REA version

Exact node scripts/rea.mjs --version output:

6.8.0

Source commit: 65f1c447344a423fa2785847dbcea1e9a9e2454a.

Runtime and platform

Node.js v24.22.1; macOS 27.0.1; Darwin arm64. No provider or GUI application was launched.

Target and artifact context

Fresh isolated Claude Code personal skill destination; SKILL.md and seven managed references. No binary or reverse-engineering target.

Interface

CLI

Earliest failure stage

Setup or provider admission

Relevant output or evidence

A failed process returned these fields; only the generated home path is replaced with <HOME>:

{
  "status": "needs_human",
  "code": "skill_install_failed",
  "appliedActions": [],
  "remediation": "REA skill write failed at <HOME>/.claude/skills/reverse-engineer-anything/SKILL.md: Selected input changed while being read: <HOME>/.claude/skills/reverse-engineer-anything/SKILL.md. Check permissions and rerun setup."
}

The harness printed managed files: 8, canonical: True, staging directories: 0; its sequential retry returned exit 0, ready, no code/remediation and appliedActions: []. These observations establish successful preservation and recovery by retry, rather than a permission problem.

Baseline contract and implementation locators:

  • docs/installation.md:180-198: first-install concurrent-creation refusal, preservation/recovery and advice against overlapping installers.
  • AGENTS.md:33: preserve meaningful failure reasons.
  • src/application/SkillFilePublication.ts:145-151,226-235: publication checks whether an originally absent destination has appeared and throws RegularFileChangedError before attempting the exclusive link.
  • src/filesystem/RegularFile.ts:55-62: that error has the fixed input-read wording.
  • src/application/SetupSkill.ts:142-144: every skill install failure receives the permission-check sentence.
  • src/application/Setup.ts:116-122: setup projects the skill failure as skill_install_failed.
  • tests/boundary/filesystem/setupSkillPublication.test.ts:103-125: an existing control verifies that a concurrently created first-install file survives refusal; it does not assert cause/remedy wording. This control was inspected, not executed in this verification.

Typed outcome or recovery expected

Keep the refusal and file-preservation behavior. Explain that the destination appeared or changed during installation, and tell the caller to retry after concurrent setup activity stops. A new result code is not necessary to address the demonstrated wording defect.

Analysis or suspected cause

Source-derived explanation: the publication guard reuses a regular-file reading error for a destination-identity check. The setup error presentation then appends permission advice regardless of the reason. The CLI reproduction matches that path: failures name the write stage and the read-time error while a sibling publishes the previously absent file.

Additional context

Duplicate searches covered open/closed issues, issue comments and open/closed/merged PRs. Issue #5928 concerns rollback overwriting concurrent edits; its closure describes ownership and preservation fixes. PR #5936 now adds alias and cleanup-retry coverage. Their acceptance criteria differ from this message-only defect; this report does not claim their repaired data-preservation problem regressed.

Other related results were configuration ownership diagnostics (#6430), unreadable-skill doctor classification (#7091), uninstall path omission (#2111), and skill backup/cleanup work (#2959, #6920). None describes the same first-install refusal message. No checkout files were changed and nothing was published during verification.

Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7618 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Setup, packaging, or upgrade ### Observed behavior I ran six `node scripts/rea.mjs setup --yes --skill-client claude_code --json` processes against one fresh isolated HOME. One returned `ready` and installed the skill. Five returned `needs_human`, code `skill_install_failed`, with a write-stage message saying the selected input changed while being read and advising a permission check. The destination was being created by sibling setup processes. The final eight managed files matched the bundled files byte for byte, and no skill staging directory remained. Running the same command sequentially afterward returned `ready` with no applied actions and no permission change. Two independent six-process rounds produced this same 1/5 split, with every refusal naming `SKILL.md`. ### Reproduction 1. Start in the built baseline checkout with Node.js v24.22.1 first on PATH. The checkout's existing runtime and bundled skill must already be built. No dependency installation is part of this reproduction. 2. Run this Python harness from the checkout root. It discards inherited client overrides, uses one fresh home for the six processes, isolates HOME/XDG/npm/temp paths, and puts recording-only `npx`, `npm` and `bun` fixtures first on PATH before setup. It deletes its temporary tree afterward. ```sh python3 -I - <<'PY_REPRO' import concurrent.futures, json, pathlib, shutil, subprocess, tempfile repo = pathlib.Path.cwd() node = str(pathlib.Path(shutil.which('node')).resolve()) with tempfile.TemporaryDirectory() as trial: root = pathlib.Path(trial) home, fixtures = root / 'home', root / 'bin' home.mkdir(); fixtures.mkdir() runner = '''#!/bin/sh printf '%s' "${0##*/}" >> "$FIXTURE_LOG" for arg in "$@"; do printf ' <%s>' "$arg" >> "$FIXTURE_LOG"; done printf '\\n' >> "$FIXTURE_LOG" printf '6.8.0\\n' ''' for name in ('npx', 'npm', 'bun'): executable = fixtures / name executable.write_text(runner); executable.chmod(0o755) env = {'HOME': str(home), 'PATH': f'{fixtures}:{pathlib.Path(node).parent}:/usr/bin:/bin', 'FIXTURE_LOG': str(root / 'runners.log'), 'CI': '1', 'NO_COLOR': '1'} for key, name in (('XDG_CONFIG_HOME', 'config'), ('XDG_CACHE_HOME', 'cache'), ('XDG_DATA_HOME', 'data'), ('XDG_STATE_HOME', 'state'), ('TMPDIR', 'tmp'), ('npm_config_prefix', 'npm-global')): directory = home / name; directory.mkdir(); env[key] = str(directory) for name in ('npx', 'npm', 'bun'): assert shutil.which(name, path=env['PATH']) == str(fixtures / name) cli = [node, str(repo / 'scripts/rea.mjs')] print('REA:', subprocess.check_output(cli + ['--version'], env=env, cwd=home, text=True).strip()) def run(_): result = subprocess.run(cli + ['setup', '--yes', '--skill-client', 'claude_code', '--json'], cwd=home, env=env, capture_output=True, text=True, timeout=90) data = json.loads(result.stdout) return {'exit': result.returncode, 'status': data['status'], 'code': data.get('code'), 'remediation': data.get('remediation'), 'appliedActions': data['appliedActions']} with concurrent.futures.ThreadPoolExecutor(max_workers=6) as workers: results = list(workers.map(run, range(6))) print(json.dumps(results, indent=2).replace(str(home.resolve()), '<HOME>')) skill = home / '.claude/skills/reverse-engineer-anything' files = [p for p in skill.rglob('*') if p.is_file()] print('managed files:', len(files)) print('canonical:', all(p.read_bytes() == (repo / 'skills/reverse-engineer-anything' / p.relative_to(skill)).read_bytes() for p in files)) print('staging directories:', len(list(home.rglob('.rea-skill-*')))) print('sequential retry:', json.dumps(run(None))) print('package runners:', (root / 'runners.log').read_text() if (root / 'runners.log').exists() else 'none invoked') PY_REPRO ``` 3. Inspect the six result objects, final file checks and sequential retry. In both executed rounds: one `ready`/exit 0, five `needs_human`/exit 1 with the diagnostic below, eight canonical files, zero staging directories, and a `ready` sequential retry with `appliedActions: []`. Neither round invoked a package runner. The inline harness itself was executed in the second round. This is a scheduling-dependent race; exact counts and named files may vary on another run. ### Expected behavior The installation documentation explicitly says setup refuses a concurrently created destination and advises against overlapping installers. The refusal itself is expected. Its explanation should identify the changed destination or concurrent creation and recommend retrying after the other installer finishes. The repository also requires meaningful failure reasons. A publication-time destination conflict should not be described as an input changing during a read with generic permission-check advice. The documentation does not promise that every concurrent process succeeds or require a particular skill conflict enum. ### Impact and scope P3: misleading diagnostic wording. The losing caller receives an inaccurate stage explanation and unnecessary permission advice, although the same message also recommends rerunning setup, which worked in both rounds. No corruption, permission failure or unfinished staging cleanup was observed. The successful installation remained complete. Verified scope is first installation of the same bundled skill by six processes on one macOS host. Different REA versions, existing-file update/backup races and other platforms were not exercised. ### REA version Exact `node scripts/rea.mjs --version` output: ```text 6.8.0 ``` Source commit: `65f1c447344a423fa2785847dbcea1e9a9e2454a`. ### Runtime and platform Node.js `v24.22.1`; macOS `27.0.1`; Darwin arm64. No provider or GUI application was launched. ### Target and artifact context Fresh isolated Claude Code personal skill destination; `SKILL.md` and seven managed references. No binary or reverse-engineering target. ### Interface CLI ### Earliest failure stage Setup or provider admission ### Relevant output or evidence A failed process returned these fields; only the generated home path is replaced with `<HOME>`: ```json { "status": "needs_human", "code": "skill_install_failed", "appliedActions": [], "remediation": "REA skill write failed at <HOME>/.claude/skills/reverse-engineer-anything/SKILL.md: Selected input changed while being read: <HOME>/.claude/skills/reverse-engineer-anything/SKILL.md. Check permissions and rerun setup." } ``` The harness printed `managed files: 8`, `canonical: True`, `staging directories: 0`; its sequential retry returned exit 0, `ready`, no code/remediation and `appliedActions: []`. These observations establish successful preservation and recovery by retry, rather than a permission problem. Baseline contract and implementation locators: - `docs/installation.md:180-198`: first-install concurrent-creation refusal, preservation/recovery and advice against overlapping installers. - `AGENTS.md:33`: preserve meaningful failure reasons. - `src/application/SkillFilePublication.ts:145-151,226-235`: publication checks whether an originally absent destination has appeared and throws `RegularFileChangedError` before attempting the exclusive link. - `src/filesystem/RegularFile.ts:55-62`: that error has the fixed input-read wording. - `src/application/SetupSkill.ts:142-144`: every skill install failure receives the permission-check sentence. - `src/application/Setup.ts:116-122`: setup projects the skill failure as `skill_install_failed`. - `tests/boundary/filesystem/setupSkillPublication.test.ts:103-125`: an existing control verifies that a concurrently created first-install file survives refusal; it does not assert cause/remedy wording. This control was inspected, not executed in this verification. ### Typed outcome or recovery expected Keep the refusal and file-preservation behavior. Explain that the destination appeared or changed during installation, and tell the caller to retry after concurrent setup activity stops. A new result code is not necessary to address the demonstrated wording defect. ### Analysis or suspected cause Source-derived explanation: the publication guard reuses a regular-file reading error for a destination-identity check. The setup error presentation then appends permission advice regardless of the reason. The CLI reproduction matches that path: failures name the write stage and the read-time error while a sibling publishes the previously absent file. ### Additional context Duplicate searches covered open/closed issues, issue comments and open/closed/merged PRs. [Issue #5928](https://github.com/morluto/rea/issues/5928) concerns rollback overwriting concurrent edits; its closure describes ownership and preservation fixes. [PR #5936](https://github.com/morluto/rea/pull/5936) now adds alias and cleanup-retry coverage. Their acceptance criteria differ from this message-only defect; this report does not claim their repaired data-preservation problem regressed. Other related results were configuration ownership diagnostics (#6430), unreadable-skill doctor classification (#7091), uninstall path omission (#2111), and skill backup/cleanup work (#2959, #6920). None describes the same first-install refusal message. No checkout files were changed and nothing was published during verification.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#4547
No description provided.