[GH-ISSUE #4244] [Bug] Cursor uninstall fails when the sole REA server has a trailing comma #1667

Closed
opened 2026-10-16 21:08:47 +00:00 by bsakaguchi · 2 comments
Owner

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

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

REA accepts a setup-created Cursor registration as aligned but cannot uninstall it when it is the only entry in mcpServers and has a trailing comma. uninstall --json exits 1 with overall status failed and Cursor status failed. It restores the original file and leaves REA registered. Default-format uninstall also exits 1 and reports cursor,failed.

Removing only the comma preserves the parsed JSONC values and doctor alignment, then the same public uninstall succeeds. The first backup remains unchanged through failure and recovery. No data corruption was observed.

Reproduction

Use the source revision and pinned toolchain below, install locked dependencies with npm ci, and build with npm run build:cached. Run this exact executed recipe from the checkout root. It creates disposable homes, constructs each CLI environment from scratch, previews and checks all planned client paths before apply, and uses real setup to establish ownership. It launches no external client and deletes the disposable homes in finally.

node --input-type=module <<'JS'
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve, relative, dirname, sep } from 'node:path';
import { spawnSync } from 'node:child_process';
import { createRequire } from 'node:module';
const checkout = process.cwd();
const cli = resolve('scripts/rea.mjs');
const { parse, parseTree, findNodeAtLocation } = createRequire(resolve('package.json'))('jsonc-parser');
const root = mkdtempSync(join(tmpdir(), 'rea-jsonc-'));
function value(text) {
  const errors = [];
  const result = parse(text, errors, { allowTrailingComma: true });
  assert.deepEqual(errors, []);
  return result;
}
try {
  for (const [name, comma, position, rich] of [
    ['single-comma', true, 'only', false],
    ['single-clean', false, 'only', false],
    ['two-rea-last', true, 'last', false],
    ['two-rea-first', true, 'first', false],
    ['unrelated-comma', true, 'only', true],
    ['unrelated-clean', false, 'only', true],
  ]) {
    const home = join(root, name);
    mkdirSync(join(home, '.cursor'), { recursive: true });
    mkdirSync(join(home, 'temp'));
    const file = join(home, '.cursor/mcp.json');
    const seed = rich ? '{\n// Keep theme choice.\n"theme":"dark",\n"metadata":{"enabled":true,"levels":[1,2,],},\n"mcpServers":{}\n}\n' : '{"mcpServers":{}}\n';
    writeFileSync(file, seed);
    const env = {
      PATH: `${dirname(process.execPath)}:/usr/bin:/bin:/usr/sbin:/sbin`, HOME: home,
      XDG_CONFIG_HOME: join(home, '.config'), XDG_CACHE_HOME: join(home, '.cache'),
      XDG_STATE_HOME: join(home, '.local/state'), XDG_DATA_HOME: join(home, '.local/share'),
      XDG_RUNTIME_DIR: join(home, 'runtime'), TMPDIR: join(home, 'temp'),
      npm_config_cache: join(home, '.npm-cache'), NO_COLOR: '1',
      GEMINI_CLI_SYSTEM_SETTINGS_PATH: join(home, 'system/settings.json'),
      GEMINI_CLI_SYSTEM_DEFAULTS_PATH: join(home, 'system/defaults.json'),
    };
    function run(...args) {
      const p = spawnSync(process.execPath, [cli, ...args, '--json'], { cwd: home, env, encoding: 'utf8', timeout: 30000, maxBuffer: 8 * 1024 * 1024 });
      assert.ifError(p.error);
      assert.equal(p.signal, null);
      return { exit: p.status, data: JSON.parse(p.stdout) };
    }
    const setup = ['setup', '--client', 'cursor', '--skill=false'];
    const plan = run(...setup, '--dry-run');
    assert.equal(plan.exit, 0);
    assert.equal(plan.data.status, 'planned');
    assert.equal(readFileSync(file, 'utf8'), seed);
    assert.deepEqual(plan.data.plannedActions.map(a => [a.kind, a.target]), [['configure_client', file]]);
    for (const state of plan.data.clientStates) {
      for (const path of [state.client.configPath, ...(state.client.configPaths || []), state.client.markerPath].filter(Boolean)) {
        const rel = relative(home, path);
        assert(rel !== '..' && !rel.startsWith(`..${sep}`));
      }
    }
    const applied = run(...setup, '--yes');
    assert.equal(applied.exit, 0);
    assert.equal(applied.data.status, 'ready');
    assert.deepEqual(applied.data.appliedActions, ['configured_cursor']);
    function doctor() {
      const p = run('doctor', '--client', 'cursor');
      const state = p.data.identity.registrations.find(r => r.client === 'cursor').state;
      assert.equal(p.exit, 0);
      assert.equal(state, 'aligned');
      return state;
    }
    doctor();
    let text = readFileSync(file, 'utf8');
    const owned = value(text).mcpServers.rea;
    assert.equal(owned.command, cli);
    assert.deepEqual(owned.args, ['mcp']);
    if (position !== 'only') {
      const doc = value(text), other = { command: 'example-other', args: ['serve'] };
      doc.mcpServers = position === 'first' ? { rea: owned, other } : { other, rea: owned };
      text = JSON.stringify(doc, null, 2) + '\n';
    }
    if (comma) {
      const original = value(text);
      const key = position === 'first' ? 'other' : 'rea';
      const node = findNodeAtLocation(parseTree(text), ['mcpServers', key]);
      const end = node.offset + node.length;
      text = text.slice(0, end) + ',' + text.slice(end);
      assert.deepEqual(value(text), original);
    }
    writeFileSync(file, text);
    const state = doctor();
    const backup = readFileSync(file + '.rea.backup', 'utf8');
    const expected = value(text);
    delete expected.mcpServers.rea;
    const removed = run('uninstall');
    const item = removed.data.items.find(i => i.name === 'cursor');
    const after = readFileSync(file, 'utf8');
    assert.equal(readFileSync(file + '.rea.backup', 'utf8'), backup);
    if (item.status === 'failed') assert.equal(after, text);
    else assert.deepEqual(value(after), expected);
    console.log(JSON.stringify({ name, doctor: state, exit: removed.exit, status: removed.data.status, cursor: item.status, unchanged: after === text }));
    if (item.status === 'failed' && position === 'only' && comma) {
      const node = findNodeAtLocation(parseTree(text, [], { allowTrailingComma: true }), ['mcpServers', 'rea']);
      const end = node.offset + node.length;
      assert.equal(text[end], ',');
      const repaired = text.slice(0, end) + text.slice(end + 1);
      assert.deepEqual(value(repaired), value(text));
      writeFileSync(file, repaired);
      doctor();
      const retry = run('uninstall');
      assert.equal(retry.exit, 0);
      assert.equal(retry.data.items.find(i => i.name === 'cursor').status, 'removed');
      assert.deepEqual(value(readFileSync(file, 'utf8')), expected);
      assert.equal(readFileSync(file + '.rea.backup', 'utf8'), backup);
      console.log(JSON.stringify({ name, workaroundValuesIdentical: true, retryExit: retry.exit, retryStatus: retry.data.status, retryCursor: 'removed' }));
    }
    if (rich) assert(readFileSync(file, 'utf8').includes('// Keep theme choice.'));
  }
} finally {
  rmSync(root, { recursive: true, force: true });
}
JS

single-comma is the reduced failing case: the only edit after setup is one comma after the sole REA property's value. unrelated-comma adds a theme, nested metadata, unrelated trailing commas, and a comment. The two-entry controls retain an inert example-other registration, which is configuration text and is never launched. The script prints actual outcomes; the failure and control results are below.

Expected behavior

REA should remove its owned registration from the accepted JSONC document, report successful removal, and leave valid configuration with unrelated values and documented comments preserved. Cursor's adapter explicitly accepts comments and trailing commas. The installation contract promises owned-registration removal and preservation of other servers and data.

Impact and scope

P2, supported. A bounded supported Cursor configuration shape blocks registration removal and leaves REA registered. Both sole-entry comma fixtures fail, while both sole-entry no-comma controls and both two-entry controls succeed. The observed rollback preserves the original bytes and backup.

A verified workaround is available: remove only the comma immediately after the sole REA property's value. On both failing fixtures, JSONC values remain identical, scoped doctor stays aligned, and uninstall then succeeds while preserving unrelated values, the enriched fixture's comment, and the original backup.

This covers the built source-checkout CLI and REA's Cursor adapter. Native Cursor, installed-release artifacts, other client adapters, and other platforms were not exercised. NOT_ENOUGH_INFO for packaged-release applicability and the first affected release; no release artifact or known-good release was tested.

REA version

Built source checkout at 25c22222abe7a681491bdb4d7316250fdcf32973 on current upstream main. The CLI reports 6.5.0; this identifies source-checkout behavior and does not establish behavior of the published 6.5.0 package.

Runtime and platform

Node.js v24.18.0; npm 11.16.0; macOS arm64. The runtime was built with the repository's pinned toolchain and locked dependencies. No analysis engine or native Cursor process was launched.

Target and artifact context

Disposable regular .cursor/mcp.json files in isolated HOME/XDG/config environments. Real setup --client cursor --skill=false creates the owned registration. The same absolute launcher is used for setup, scoped doctor, and uninstall. No binary target is involved.

Interface

CLI

Earliest failure stage

Cleanup, cancellation, or lifecycle

Relevant output or evidence

The exact public recipe was executed on the source revision above. Setup returned ready and scoped doctor returned aligned with exit 0 for every case before uninstall. It produced:

single-comma:     uninstall exit 1, status failed, cursor failed, original bytes unchanged
single-clean:     uninstall exit 0, status complete, cursor removed
two-rea-last:     uninstall exit 0, status complete, cursor removed, other server preserved
two-rea-first:    uninstall exit 0, status complete, cursor removed, other server preserved
unrelated-comma:  uninstall exit 1, status failed, cursor failed, original bytes unchanged
unrelated-clean:  uninstall exit 0, status complete, cursor removed, other values/comment preserved

A separate public lifecycle replay on the same build also exercised default-format uninstall for both failing fixtures. Both exited 1 and reported cursor,failed. Both JSON-format failure items reported:

Configuration could not be updated. The original was restored and its `.rea.backup` was retained. Repair the configuration or restore the backup, then rerun uninstall.

Both comma-only workaround retries returned exit 0, overall status complete, and Cursor status removed. Assertions checked equal JSONC values before and after the comma edit, continued doctor alignment, preserved unrelated values and comments, and the unchanged first backup. The enclosing diagnostic scripts exited 0 because their checks completed; the failing uninstall child commands exited 1.

Typed outcome or recovery expected

Rerun the unchanged sole-entry comma inputs through public uninstall. Require exit 0, successful Cursor removal, valid supported configuration with the owned registration absent, unrelated values preserved, and documented comments retained. Keep the single-entry no-comma and two-entry controls passing, including preservation of the unrelated server and backup. Restoration must accept the original comma-bearing inputs without requiring the workaround. No repaired build was tested.

Analysis or suspected cause

Source-derived: JSONC deletion and serialization defers first-property deletion to jsonc-parser's stock edit. This is consistent with retaining a trailing comma after deleting the only property, followed by readback rejection and rollback. The public lifecycle failure establishes the defect independently of this explanation. The implementation may choose any deletion or serialization approach that preserves the stated behavior; exact whitespace is not required.

Additional context

The current-source replay confirmed the failure after fetching upstream main. The cited Cursor adapter, JSONC serializer, ownership checks, uninstall transaction, and installation contract were unchanged from the independently verified source baseline.

Related reports have different triggers and closure signals: #3862 and #3863 concern Codex per-tool tables during doctor/setup; #3906 concerns switching CLI aliases, while this recipe uses one launcher and remains aligned; #3301 concerns positive uninstall fixtures with unrecognized commands, while this recipe uses real setup; #1164 concerns already-malformed input, while this input is valid before removal; #3308 concerns skill-manifest removal status rather than client JSONC deletion.

Originally created by @coygeek on GitHub (Oct 15, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/4244 ### 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 REA accepts a setup-created Cursor registration as aligned but cannot uninstall it when it is the only entry in `mcpServers` and has a trailing comma. `uninstall --json` exits 1 with overall status `failed` and Cursor status `failed`. It restores the original file and leaves REA registered. Default-format uninstall also exits 1 and reports `cursor,failed`. Removing only the comma preserves the parsed JSONC values and doctor alignment, then the same public uninstall succeeds. The first backup remains unchanged through failure and recovery. No data corruption was observed. ### Reproduction Use the source revision and pinned toolchain below, install locked dependencies with `npm ci`, and build with `npm run build:cached`. Run this exact executed recipe from the checkout root. It creates disposable homes, constructs each CLI environment from scratch, previews and checks all planned client paths before apply, and uses real setup to establish ownership. It launches no external client and deletes the disposable homes in `finally`. ```sh node --input-type=module <<'JS' import assert from 'node:assert/strict'; import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join, resolve, relative, dirname, sep } from 'node:path'; import { spawnSync } from 'node:child_process'; import { createRequire } from 'node:module'; const checkout = process.cwd(); const cli = resolve('scripts/rea.mjs'); const { parse, parseTree, findNodeAtLocation } = createRequire(resolve('package.json'))('jsonc-parser'); const root = mkdtempSync(join(tmpdir(), 'rea-jsonc-')); function value(text) { const errors = []; const result = parse(text, errors, { allowTrailingComma: true }); assert.deepEqual(errors, []); return result; } try { for (const [name, comma, position, rich] of [ ['single-comma', true, 'only', false], ['single-clean', false, 'only', false], ['two-rea-last', true, 'last', false], ['two-rea-first', true, 'first', false], ['unrelated-comma', true, 'only', true], ['unrelated-clean', false, 'only', true], ]) { const home = join(root, name); mkdirSync(join(home, '.cursor'), { recursive: true }); mkdirSync(join(home, 'temp')); const file = join(home, '.cursor/mcp.json'); const seed = rich ? '{\n// Keep theme choice.\n"theme":"dark",\n"metadata":{"enabled":true,"levels":[1,2,],},\n"mcpServers":{}\n}\n' : '{"mcpServers":{}}\n'; writeFileSync(file, seed); const env = { PATH: `${dirname(process.execPath)}:/usr/bin:/bin:/usr/sbin:/sbin`, HOME: home, XDG_CONFIG_HOME: join(home, '.config'), XDG_CACHE_HOME: join(home, '.cache'), XDG_STATE_HOME: join(home, '.local/state'), XDG_DATA_HOME: join(home, '.local/share'), XDG_RUNTIME_DIR: join(home, 'runtime'), TMPDIR: join(home, 'temp'), npm_config_cache: join(home, '.npm-cache'), NO_COLOR: '1', GEMINI_CLI_SYSTEM_SETTINGS_PATH: join(home, 'system/settings.json'), GEMINI_CLI_SYSTEM_DEFAULTS_PATH: join(home, 'system/defaults.json'), }; function run(...args) { const p = spawnSync(process.execPath, [cli, ...args, '--json'], { cwd: home, env, encoding: 'utf8', timeout: 30000, maxBuffer: 8 * 1024 * 1024 }); assert.ifError(p.error); assert.equal(p.signal, null); return { exit: p.status, data: JSON.parse(p.stdout) }; } const setup = ['setup', '--client', 'cursor', '--skill=false']; const plan = run(...setup, '--dry-run'); assert.equal(plan.exit, 0); assert.equal(plan.data.status, 'planned'); assert.equal(readFileSync(file, 'utf8'), seed); assert.deepEqual(plan.data.plannedActions.map(a => [a.kind, a.target]), [['configure_client', file]]); for (const state of plan.data.clientStates) { for (const path of [state.client.configPath, ...(state.client.configPaths || []), state.client.markerPath].filter(Boolean)) { const rel = relative(home, path); assert(rel !== '..' && !rel.startsWith(`..${sep}`)); } } const applied = run(...setup, '--yes'); assert.equal(applied.exit, 0); assert.equal(applied.data.status, 'ready'); assert.deepEqual(applied.data.appliedActions, ['configured_cursor']); function doctor() { const p = run('doctor', '--client', 'cursor'); const state = p.data.identity.registrations.find(r => r.client === 'cursor').state; assert.equal(p.exit, 0); assert.equal(state, 'aligned'); return state; } doctor(); let text = readFileSync(file, 'utf8'); const owned = value(text).mcpServers.rea; assert.equal(owned.command, cli); assert.deepEqual(owned.args, ['mcp']); if (position !== 'only') { const doc = value(text), other = { command: 'example-other', args: ['serve'] }; doc.mcpServers = position === 'first' ? { rea: owned, other } : { other, rea: owned }; text = JSON.stringify(doc, null, 2) + '\n'; } if (comma) { const original = value(text); const key = position === 'first' ? 'other' : 'rea'; const node = findNodeAtLocation(parseTree(text), ['mcpServers', key]); const end = node.offset + node.length; text = text.slice(0, end) + ',' + text.slice(end); assert.deepEqual(value(text), original); } writeFileSync(file, text); const state = doctor(); const backup = readFileSync(file + '.rea.backup', 'utf8'); const expected = value(text); delete expected.mcpServers.rea; const removed = run('uninstall'); const item = removed.data.items.find(i => i.name === 'cursor'); const after = readFileSync(file, 'utf8'); assert.equal(readFileSync(file + '.rea.backup', 'utf8'), backup); if (item.status === 'failed') assert.equal(after, text); else assert.deepEqual(value(after), expected); console.log(JSON.stringify({ name, doctor: state, exit: removed.exit, status: removed.data.status, cursor: item.status, unchanged: after === text })); if (item.status === 'failed' && position === 'only' && comma) { const node = findNodeAtLocation(parseTree(text, [], { allowTrailingComma: true }), ['mcpServers', 'rea']); const end = node.offset + node.length; assert.equal(text[end], ','); const repaired = text.slice(0, end) + text.slice(end + 1); assert.deepEqual(value(repaired), value(text)); writeFileSync(file, repaired); doctor(); const retry = run('uninstall'); assert.equal(retry.exit, 0); assert.equal(retry.data.items.find(i => i.name === 'cursor').status, 'removed'); assert.deepEqual(value(readFileSync(file, 'utf8')), expected); assert.equal(readFileSync(file + '.rea.backup', 'utf8'), backup); console.log(JSON.stringify({ name, workaroundValuesIdentical: true, retryExit: retry.exit, retryStatus: retry.data.status, retryCursor: 'removed' })); } if (rich) assert(readFileSync(file, 'utf8').includes('// Keep theme choice.')); } } finally { rmSync(root, { recursive: true, force: true }); } JS ``` `single-comma` is the reduced failing case: the only edit after setup is one comma after the sole REA property's value. `unrelated-comma` adds a theme, nested metadata, unrelated trailing commas, and a comment. The two-entry controls retain an inert `example-other` registration, which is configuration text and is never launched. The script prints actual outcomes; the failure and control results are below. ### Expected behavior REA should remove its owned registration from the accepted JSONC document, report successful removal, and leave valid configuration with unrelated values and documented comments preserved. Cursor's adapter explicitly accepts comments and trailing commas. The installation contract promises owned-registration removal and preservation of other servers and data. ### Impact and scope P2, supported. A bounded supported Cursor configuration shape blocks registration removal and leaves REA registered. Both sole-entry comma fixtures fail, while both sole-entry no-comma controls and both two-entry controls succeed. The observed rollback preserves the original bytes and backup. A verified workaround is available: remove only the comma immediately after the sole REA property's value. On both failing fixtures, JSONC values remain identical, scoped doctor stays aligned, and uninstall then succeeds while preserving unrelated values, the enriched fixture's comment, and the original backup. This covers the built source-checkout CLI and REA's Cursor adapter. Native Cursor, installed-release artifacts, other client adapters, and other platforms were not exercised. NOT_ENOUGH_INFO for packaged-release applicability and the first affected release; no release artifact or known-good release was tested. ### REA version Built source checkout at `25c22222abe7a681491bdb4d7316250fdcf32973` on current upstream main. The CLI reports 6.5.0; this identifies source-checkout behavior and does not establish behavior of the published 6.5.0 package. ### Runtime and platform Node.js v24.18.0; npm 11.16.0; macOS arm64. The runtime was built with the repository's pinned toolchain and locked dependencies. No analysis engine or native Cursor process was launched. ### Target and artifact context Disposable regular `.cursor/mcp.json` files in isolated HOME/XDG/config environments. Real `setup --client cursor --skill=false` creates the owned registration. The same absolute launcher is used for setup, scoped doctor, and uninstall. No binary target is involved. ### Interface CLI ### Earliest failure stage Cleanup, cancellation, or lifecycle ### Relevant output or evidence The exact public recipe was executed on the source revision above. Setup returned `ready` and scoped doctor returned `aligned` with exit 0 for every case before uninstall. It produced: ```text single-comma: uninstall exit 1, status failed, cursor failed, original bytes unchanged single-clean: uninstall exit 0, status complete, cursor removed two-rea-last: uninstall exit 0, status complete, cursor removed, other server preserved two-rea-first: uninstall exit 0, status complete, cursor removed, other server preserved unrelated-comma: uninstall exit 1, status failed, cursor failed, original bytes unchanged unrelated-clean: uninstall exit 0, status complete, cursor removed, other values/comment preserved ``` A separate public lifecycle replay on the same build also exercised default-format uninstall for both failing fixtures. Both exited 1 and reported `cursor,failed`. Both JSON-format failure items reported: ```text Configuration could not be updated. The original was restored and its `.rea.backup` was retained. Repair the configuration or restore the backup, then rerun uninstall. ``` Both comma-only workaround retries returned exit 0, overall status `complete`, and Cursor status `removed`. Assertions checked equal JSONC values before and after the comma edit, continued doctor alignment, preserved unrelated values and comments, and the unchanged first backup. The enclosing diagnostic scripts exited 0 because their checks completed; the failing uninstall child commands exited 1. - Expected source: [Cursor adapter selection](https://github.com/morluto/rea/blob/25c22222abe7a681491bdb4d7316250fdcf32973/src/application/SupportedClients.ts#L512-L517), [JSONC admission and server-table validation](https://github.com/morluto/rea/blob/25c22222abe7a681491bdb4d7316250fdcf32973/src/application/ClientConfigurationDocument.ts#L106-L225), [configuration preservation and backup contract](https://github.com/morluto/rea/blob/25c22222abe7a681491bdb4d7316250fdcf32973/docs/installation.md#L49-L65), and [owned-registration removal contract](https://github.com/morluto/rea/blob/25c22222abe7a681491bdb4d7316250fdcf32973/docs/installation.md#L351-L366). - Failure source: the executed CLI results above establish the defect. [Uninstall ownership, serialization, readback, and rollback](https://github.com/morluto/rea/blob/25c22222abe7a681491bdb4d7316250fdcf32973/src/application/Uninstall.ts#L402-L533) explain why failed readback leaves the original registration in place. - Evidence provenance: observed. - Local verification: reproduced. - Reproduction completeness: complete. ### Typed outcome or recovery expected Rerun the unchanged sole-entry comma inputs through public uninstall. Require exit 0, successful Cursor removal, valid supported configuration with the owned registration absent, unrelated values preserved, and documented comments retained. Keep the single-entry no-comma and two-entry controls passing, including preservation of the unrelated server and backup. Restoration must accept the original comma-bearing inputs without requiring the workaround. No repaired build was tested. ### Analysis or suspected cause Source-derived: [JSONC deletion and serialization](https://github.com/morluto/rea/blob/25c22222abe7a681491bdb4d7316250fdcf32973/src/application/ClientConfigurationDocument.ts#L1287-L1388) defers first-property deletion to `jsonc-parser`'s stock edit. This is consistent with retaining a trailing comma after deleting the only property, followed by readback rejection and rollback. The public lifecycle failure establishes the defect independently of this explanation. The implementation may choose any deletion or serialization approach that preserves the stated behavior; exact whitespace is not required. ### Additional context The current-source replay confirmed the failure after fetching upstream main. The cited Cursor adapter, JSONC serializer, ownership checks, uninstall transaction, and installation contract were unchanged from the independently verified source baseline. Related reports have different triggers and closure signals: [#3862](https://github.com/morluto/rea/issues/3862) and [#3863](https://github.com/morluto/rea/issues/3863) concern Codex per-tool tables during doctor/setup; [#3906](https://github.com/morluto/rea/issues/3906) concerns switching CLI aliases, while this recipe uses one launcher and remains aligned; [#3301](https://github.com/morluto/rea/issues/3301) concerns positive uninstall fixtures with unrecognized commands, while this recipe uses real setup; [#1164](https://github.com/morluto/rea/issues/1164) concerns already-malformed input, while this input is valid before removal; [#3308](https://github.com/morluto/rea/issues/3308) concerns skill-manifest removal status rather than client JSONC deletion.
Author
Owner

@official-rex commented on GitHub (Oct 15, 2026):

I can fix this Cursor uninstall issue. Kindly assign me this task; I look forward to your response.

<!-- gh-comment-id:6202295549 --> @official-rex commented on GitHub (Oct 15, 2026): I can fix this Cursor uninstall issue. Kindly assign me this task; I look forward to your response.
Author
Owner

@morluto commented on GitHub (Oct 16, 2026):

Fixed on main in 4fd28fc9e. JSONC unregistration now removes parsed property/separator spans, including a sole trailing-comma member, while retaining unrelated trivia and backups.

JSONC regressions cover the reported uninstall case, BOM/CRLF/comments, siblings and idempotent removal.

Hosted main CI is still pending; this closure is based on the implemented fix and the scoped regression evidence.

<!-- gh-comment-id:6206035024 --> @morluto commented on GitHub (Oct 16, 2026): Fixed on `main` in [4fd28fc9e](https://github.com/morluto/rea/commit/4fd28fc9e0c4f8358147af38c72ab9e814f52d61). JSONC unregistration now removes parsed property/separator spans, including a sole trailing-comma member, while retaining unrelated trivia and backups. JSONC regressions cover the reported uninstall case, BOM/CRLF/comments, siblings and idempotent removal. Hosted main CI is still pending; this closure is based on the implemented fix and the scoped regression evidence.
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#1667
No description provided.