[GH-ISSUE #7561] [Bug] doctor accepts an empty --target and passes target readiness for cwd #4498

Open
opened 2026-10-31 00:50:18 +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/7561

Before submitting

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

Area

CLI

Observed behavior

From a synthetic application bundle, node scripts/rea.mjs doctor --provider ida --target= --json returned exit 0 and healthy: true. Its selected target remained the empty string, and its target check returned ok: true, classification: "healthy" and detail: "". The separate-argument spelling --target "" did the same. Explicit --target . also passed.

From an ordinary directory, the empty target instead produced unsupported_target with directory_requires_file. Doctor therefore admits the working directory before deciding whether the selected target is valid. Omitting --target produced scope.target: null and no target check, which is distinct from the passing empty-target check.

The replay uses a syntactically valid IDA registration with a deliberately nonlaunchable command. Doctor checks that registration without starting an analysis engine, so no IDA installation is required.

Reproduction

  1. Use the built baseline checkout on macOS with Node 24.22.1 first on PATH. From the checkout root, run this block. It creates all fixtures inline, isolates HOME and XDG state, invokes the public dispatcher, and removes the fixture directory afterward. No GUI application or target is started.
python3 - <<'PYTHON'
import json, os, pathlib, shutil, subprocess, tempfile

cli = pathlib.Path('scripts/rea.mjs').resolve()
with tempfile.TemporaryDirectory(prefix='rea-doctor-empty-') as directory:
    root = pathlib.Path(directory).resolve()
    bundle = root / 'Example.app'
    (bundle / 'Contents/MacOS').mkdir(parents=True)
    shutil.copyfile('/bin/ls', bundle / 'Contents/MacOS/Example')
    (bundle / 'Contents/Info.plist').write_text(
        '<plist><dict><key>CFBundleExecutable</key><string>Example</string></dict></plist>')
    registration = root / 'ida.json'
    registration.write_text(json.dumps({'command': 'provider-must-not-launch', 'args': []}))
    env = {'PATH': os.environ['PATH'], 'REA_LOG_LEVEL': 'silent',
           'REA_IDA_MCP_CONFIG': str(registration)}
    for key, name in {
        'HOME': 'home', 'XDG_CONFIG_HOME': 'config', 'XDG_DATA_HOME': 'data',
        'XDG_CACHE_HOME': 'cache', 'XDG_STATE_HOME': 'state',
        'XDG_RUNTIME_DIR': 'runtime', 'TMPDIR': 'temp',
    }.items():
        path = root / name
        path.mkdir()
        env[key] = str(path)
    cases = [
        ('empty-equals-1', bundle, ['--target=']),
        ('empty-equals-2', bundle, ['--target=']),
        ('empty-separate', bundle, ['--target', '']),
        ('explicit-dot', bundle, ['--target', '.']),
        ('plain-empty', root, ['--target=']),
        ('omitted', bundle, []),
    ]
    for label, cwd, target in cases:
        result = subprocess.run(
            ['node', str(cli), 'doctor', '--provider', 'ida', *target, '--json'],
            cwd=cwd, env=env, capture_output=True, text=True, timeout=45)
        assert not result.stderr, result.stderr
        data = json.loads(result.stdout)
        checks = [c for c in data['scope_checks'] if c['name'] == 'target']
        row = {'case': label, 'exit': result.returncode, 'healthy': data['healthy'],
               'scope_target': data['scope']['target'], 'target_checks': checks}
        if label.startswith('empty-'):
            assert result.returncode == 0 and data['healthy'] is True
            assert checks == [{'name': 'target', 'ok': True,
                               'classification': 'healthy', 'detail': ''}]
        if label == 'explicit-dot':
            assert result.returncode == 0 and checks[0]['ok'] is True
        if label == 'plain-empty':
            assert result.returncode == 1
            assert checks[0]['details']['constraint'] == 'directory_requires_file'
        if label == 'omitted':
            assert result.returncode == 0 and checks == []
        print(json.dumps(row, separators=(',', ':')))
PYTHON
  1. The equals-empty input reproduced in both attempts; the separate empty argument reproduced once. All three returned successful target readiness. The explicit-dot, ordinary-directory and omitted-target controls each ran once and matched the assertions. Every invocation had empty stderr.

Expected behavior

Refuse an explicitly empty --target before making it relative to the working directory, and identify target as the invalid selection. Preserve intentional selection with . and omission of the optional target.

The documented CLI contract says an empty string never selects the working directory. The CLI translation invariant also requires empty selections to be rejected before path normalization. Doctor's target option selects an app path for local admission, so the rule applies to this option as well as positional target arguments.

Impact and scope

A target-specific readiness check can return success for a working-directory bundle when the caller supplied no target path. This can conceal an empty variable in a script using --target "$TARGET". The directly observed result is false successful target readiness, with different directory-specific diagnostics when cwd is an ordinary directory. No failed analysis, target execution, mutation or severe harm is claimed.

The executed scope is the built CLI on macOS, both empty spellings, an app bundle and an ordinary directory. The same behavior was independently replayed with explicit Hopper and IDA scopes. Actual provider analysis, other platforms and MCP are untested. Callers can reject an empty variable before invoking doctor and pass a nonempty intended path. Explicit . was verified for deliberate working-directory selection.

REA version

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

6.8.0

The output ends with a newline. Source baseline: 65f1c447344a423fa2785847dbcea1e9a9e2454a.

Runtime and platform

Node.js v24.22.1; macOS 27.0.1, build 26A434; Darwin arm64. The report's reproduction uses registration-only IDA diagnostics and requires no running provider.

Target and artifact context

Synthetic Example.app with an inline XML plist declaring CFBundleExecutable and a copy of the host's ls executable as parseable Mach-O bytes. The fixture executable is never run. The registration command is deliberately nonexistent; doctor validates only its configuration shape.

Interface

CLI

Earliest failure stage

Request parsing or validation

Relevant output or evidence

Observed projections of the full JSON responses from the reproduction above:

{"case":"empty-equals-1","exit":0,"healthy":true,"scope_target":"","target_ok":true,"target_classification":"healthy","target_detail":""}
{"case":"empty-equals-2","exit":0,"healthy":true,"scope_target":"","target_ok":true,"target_classification":"healthy","target_detail":""}
{"case":"empty-separate","exit":0,"healthy":true,"scope_target":"","target_ok":true,"target_classification":"healthy","target_detail":""}
{"case":"explicit-dot","exit":0,"healthy":true,"scope_target":".","target_ok":true,"target_classification":"healthy","target_detail":"."}
{"case":"plain-empty","exit":1,"healthy":false,"scope_target":"","target_ok":false,"target_classification":"unsupported_target","target_detail":"","target_constraint":"directory_requires_file"}
{"case":"omitted","exit":0,"healthy":true,"scope_target":null,"target_checks":[]}

These rows preserve the exact exit status, health, selected target and target-check facts. The ordinary-directory row projects its constraint from target_checks[0].details.constraint; the other target fields project the target check directly.

Contract locations at the baseline:

  • docs/cli.md:42-45 requires rejection of empty local paths before resolution and explicitly forbids implicit working-directory selection.
  • docs/tool-design.md:339-343 requires CLI translations to reject empty selections before normalization.
  • src/cli/setupCommands.ts:120 describes target as an optional app path and accepts a plain optional string. Its handler forwards options.target to doctor.

Source-derived explanation of the executed path:

  • src/application/DoctorDiagnostics.ts:326-337 skips the target check only for undefined, so an empty string is checked.
  • src/application/Doctor.ts:643-654 forwards the selection to parseBinaryTarget and returns a healthy target check when parsing succeeds.
  • src/application/BinaryTargetResolver.ts:142-159 makes the input absolute before filesystem admission and app-bundle resolution.
  • src/filesystem/callerPath.ts:35-47 prefixes a relative string with cwd, including an empty string.
  • src/ida/IdaDoctor.ts:17-62 validates the registration without launching IDA. The successful run with a nonexistent registration command is consistent with that boundary.

Typed outcome or recovery expected

A nonzero input-validation result identifying the empty target before directory or bundle admission. The documented contract does not require a particular error code. A later unsupported-directory failure does not satisfy rejection before resolution. Restoration should reject both empty spellings while preserving the explicit-dot and omitted-target controls.

Analysis or suspected cause

Source-derived: doctor's independent option schema admits an empty string. Relative-path anchoring then converts it into cwd before the binary-target resolver reads the bundle. The existing nonempty local-path schema in src/domain/localPath.ts:20 is used by other commands but is not attached to this option. The public CLI results establish the failure independently of this explanation.

Additional context

Related open reports #7404, #7443, #7421, #7377, #7406, #7436, #7448 and #7280 enumerate other command declarations. Their bodies and comments do not include doctor.

The complete diffs of open PR #7284 and PR #7464 change only decompile-all and observe-native-calls respectively, with their tests. They reuse an existing validator at those declarations; neither changes that validator, the shared path helper or doctor's schema. Both changes can land while the doctor reproduction remains reachable.

Closed #6757, merged PR #2687 and merged PR #5265 concern desktop, analyze/artifact and JavaScript application entrypoints. They do not establish an earlier doctor fix. This report makes no regression claim.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: The public CLI returned a successful target-readiness result after admitting a working-directory bundle for an explicit empty target. The effect is bounded to the supplied empty selection; no severe harm was observed.
  • Workaround status: available
  • Workaround basis: Callers can reject an empty target variable before invocation and supply a nonempty intended path. The executed explicit-dot control retained deliberate working-directory selection, and omission retained target-free readiness.
Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7561 ### 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 CLI ### Observed behavior From a synthetic application bundle, `node scripts/rea.mjs doctor --provider ida --target= --json` returned exit 0 and `healthy: true`. Its selected target remained the empty string, and its target check returned `ok: true`, `classification: "healthy"` and `detail: ""`. The separate-argument spelling `--target ""` did the same. Explicit `--target .` also passed. From an ordinary directory, the empty target instead produced `unsupported_target` with `directory_requires_file`. Doctor therefore admits the working directory before deciding whether the selected target is valid. Omitting --target produced `scope.target: null` and no target check, which is distinct from the passing empty-target check. The replay uses a syntactically valid IDA registration with a deliberately nonlaunchable command. Doctor checks that registration without starting an analysis engine, so no IDA installation is required. ### Reproduction 1. Use the built baseline checkout on macOS with Node 24.22.1 first on PATH. From the checkout root, run this block. It creates all fixtures inline, isolates HOME and XDG state, invokes the public dispatcher, and removes the fixture directory afterward. No GUI application or target is started. ```sh python3 - <<'PYTHON' import json, os, pathlib, shutil, subprocess, tempfile cli = pathlib.Path('scripts/rea.mjs').resolve() with tempfile.TemporaryDirectory(prefix='rea-doctor-empty-') as directory: root = pathlib.Path(directory).resolve() bundle = root / 'Example.app' (bundle / 'Contents/MacOS').mkdir(parents=True) shutil.copyfile('/bin/ls', bundle / 'Contents/MacOS/Example') (bundle / 'Contents/Info.plist').write_text( '<plist><dict><key>CFBundleExecutable</key><string>Example</string></dict></plist>') registration = root / 'ida.json' registration.write_text(json.dumps({'command': 'provider-must-not-launch', 'args': []})) env = {'PATH': os.environ['PATH'], 'REA_LOG_LEVEL': 'silent', 'REA_IDA_MCP_CONFIG': str(registration)} for key, name in { 'HOME': 'home', 'XDG_CONFIG_HOME': 'config', 'XDG_DATA_HOME': 'data', 'XDG_CACHE_HOME': 'cache', 'XDG_STATE_HOME': 'state', 'XDG_RUNTIME_DIR': 'runtime', 'TMPDIR': 'temp', }.items(): path = root / name path.mkdir() env[key] = str(path) cases = [ ('empty-equals-1', bundle, ['--target=']), ('empty-equals-2', bundle, ['--target=']), ('empty-separate', bundle, ['--target', '']), ('explicit-dot', bundle, ['--target', '.']), ('plain-empty', root, ['--target=']), ('omitted', bundle, []), ] for label, cwd, target in cases: result = subprocess.run( ['node', str(cli), 'doctor', '--provider', 'ida', *target, '--json'], cwd=cwd, env=env, capture_output=True, text=True, timeout=45) assert not result.stderr, result.stderr data = json.loads(result.stdout) checks = [c for c in data['scope_checks'] if c['name'] == 'target'] row = {'case': label, 'exit': result.returncode, 'healthy': data['healthy'], 'scope_target': data['scope']['target'], 'target_checks': checks} if label.startswith('empty-'): assert result.returncode == 0 and data['healthy'] is True assert checks == [{'name': 'target', 'ok': True, 'classification': 'healthy', 'detail': ''}] if label == 'explicit-dot': assert result.returncode == 0 and checks[0]['ok'] is True if label == 'plain-empty': assert result.returncode == 1 assert checks[0]['details']['constraint'] == 'directory_requires_file' if label == 'omitted': assert result.returncode == 0 and checks == [] print(json.dumps(row, separators=(',', ':'))) PYTHON ``` 2. The equals-empty input reproduced in both attempts; the separate empty argument reproduced once. All three returned successful target readiness. The explicit-dot, ordinary-directory and omitted-target controls each ran once and matched the assertions. Every invocation had empty stderr. ### Expected behavior Refuse an explicitly empty --target before making it relative to the working directory, and identify target as the invalid selection. Preserve intentional selection with `.` and omission of the optional target. The documented CLI contract says an empty string never selects the working directory. The CLI translation invariant also requires empty selections to be rejected before path normalization. Doctor's target option selects an app path for local admission, so the rule applies to this option as well as positional target arguments. ### Impact and scope A target-specific readiness check can return success for a working-directory bundle when the caller supplied no target path. This can conceal an empty variable in a script using `--target "$TARGET"`. The directly observed result is false successful target readiness, with different directory-specific diagnostics when cwd is an ordinary directory. No failed analysis, target execution, mutation or severe harm is claimed. The executed scope is the built CLI on macOS, both empty spellings, an app bundle and an ordinary directory. The same behavior was independently replayed with explicit Hopper and IDA scopes. Actual provider analysis, other platforms and MCP are untested. Callers can reject an empty variable before invoking doctor and pass a nonempty intended path. Explicit `.` was verified for deliberate working-directory selection. ### REA version Exact `node scripts/rea.mjs --version` stdout: ```text 6.8.0 ``` The output ends with a newline. Source baseline: `65f1c447344a423fa2785847dbcea1e9a9e2454a`. ### Runtime and platform Node.js `v24.22.1`; macOS `27.0.1`, build `26A434`; Darwin `arm64`. The report's reproduction uses registration-only IDA diagnostics and requires no running provider. ### Target and artifact context Synthetic Example.app with an inline XML plist declaring CFBundleExecutable and a copy of the host's ls executable as parseable Mach-O bytes. The fixture executable is never run. The registration command is deliberately nonexistent; doctor validates only its configuration shape. ### Interface CLI ### Earliest failure stage Request parsing or validation ### Relevant output or evidence Observed projections of the full JSON responses from the reproduction above: ```jsonl {"case":"empty-equals-1","exit":0,"healthy":true,"scope_target":"","target_ok":true,"target_classification":"healthy","target_detail":""} {"case":"empty-equals-2","exit":0,"healthy":true,"scope_target":"","target_ok":true,"target_classification":"healthy","target_detail":""} {"case":"empty-separate","exit":0,"healthy":true,"scope_target":"","target_ok":true,"target_classification":"healthy","target_detail":""} {"case":"explicit-dot","exit":0,"healthy":true,"scope_target":".","target_ok":true,"target_classification":"healthy","target_detail":"."} {"case":"plain-empty","exit":1,"healthy":false,"scope_target":"","target_ok":false,"target_classification":"unsupported_target","target_detail":"","target_constraint":"directory_requires_file"} {"case":"omitted","exit":0,"healthy":true,"scope_target":null,"target_checks":[]} ``` These rows preserve the exact exit status, health, selected target and target-check facts. The ordinary-directory row projects its constraint from `target_checks[0].details.constraint`; the other target fields project the target check directly. Contract locations at the baseline: - `docs/cli.md:42-45` requires rejection of empty local paths before resolution and explicitly forbids implicit working-directory selection. - `docs/tool-design.md:339-343` requires CLI translations to reject empty selections before normalization. - `src/cli/setupCommands.ts:120` describes target as an optional app path and accepts a plain optional string. Its handler forwards options.target to doctor. Source-derived explanation of the executed path: - `src/application/DoctorDiagnostics.ts:326-337` skips the target check only for undefined, so an empty string is checked. - `src/application/Doctor.ts:643-654` forwards the selection to parseBinaryTarget and returns a healthy target check when parsing succeeds. - `src/application/BinaryTargetResolver.ts:142-159` makes the input absolute before filesystem admission and app-bundle resolution. - `src/filesystem/callerPath.ts:35-47` prefixes a relative string with cwd, including an empty string. - `src/ida/IdaDoctor.ts:17-62` validates the registration without launching IDA. The successful run with a nonexistent registration command is consistent with that boundary. ### Typed outcome or recovery expected A nonzero input-validation result identifying the empty target before directory or bundle admission. The documented contract does not require a particular error code. A later unsupported-directory failure does not satisfy rejection before resolution. Restoration should reject both empty spellings while preserving the explicit-dot and omitted-target controls. ### Analysis or suspected cause Source-derived: doctor's independent option schema admits an empty string. Relative-path anchoring then converts it into cwd before the binary-target resolver reads the bundle. The existing nonempty local-path schema in `src/domain/localPath.ts:20` is used by other commands but is not attached to this option. The public CLI results establish the failure independently of this explanation. ### Additional context Related open reports [#7404](https://github.com/morluto/rea/issues/7404), [#7443](https://github.com/morluto/rea/issues/7443), [#7421](https://github.com/morluto/rea/issues/7421), [#7377](https://github.com/morluto/rea/issues/7377), [#7406](https://github.com/morluto/rea/issues/7406), [#7436](https://github.com/morluto/rea/issues/7436), [#7448](https://github.com/morluto/rea/issues/7448) and [#7280](https://github.com/morluto/rea/issues/7280) enumerate other command declarations. Their bodies and comments do not include doctor. The complete diffs of open [PR #7284](https://github.com/morluto/rea/pull/7284) and [PR #7464](https://github.com/morluto/rea/pull/7464) change only decompile-all and observe-native-calls respectively, with their tests. They reuse an existing validator at those declarations; neither changes that validator, the shared path helper or doctor's schema. Both changes can land while the doctor reproduction remains reachable. Closed [#6757](https://github.com/morluto/rea/issues/6757), merged [PR #2687](https://github.com/morluto/rea/pull/2687) and merged [PR #5265](https://github.com/morluto/rea/pull/5265) concern desktop, analyze/artifact and JavaScript application entrypoints. They do not establish an earlier doctor fix. This report makes no regression claim. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: The public CLI returned a successful target-readiness result after admitting a working-directory bundle for an explicit empty target. The effect is bounded to the supplied empty selection; no severe harm was observed. - Workaround status: available - Workaround basis: Callers can reject an empty target variable before invocation and supply a nonempty intended path. The executed explicit-dot control retained deliberate working-directory selection, and omission retained target-free readiness.
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#4498
No description provided.