Skip to content

Propagate user-mode setup failures via exit code across all tools#188

Merged
MohamedAklamaash merged 9 commits into
stagingfrom
fix/user-mode-setup-exit-codes
Jul 3, 2026
Merged

Propagate user-mode setup failures via exit code across all tools#188
MohamedAklamaash merged 9 commits into
stagingfrom
fix/user-mode-setup-exit-codes

Conversation

@MohamedAklamaash

@MohamedAklamaash MohamedAklamaash commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

What

Applies the same exit-code contract from the MDM fix (#171) to the user-mode setup.py installers — the ones a normal user runs interactively via the README/web-UI command:

python3 <(curl -fsSL .../<tool>/setup.py) --domain gateway.getunbound.ai

Files (9): augment/hooks, claude-code/{gateway,hooks}, codex/{gateway,hooks}, copilot/hooks, cursor, gemini-cli/gateway, openclaw.

Why

Same silent-failure class as MDM had: main() used bare return (→ None) on every error path, and __main__ called main() then exited 0 unless an exception was raised. So a failed install — missing --domain/--api-key, callback failure, env-var write failure, hook setup failure, tool-config failure — reported exit 0.

Blast radius is smaller than MDM (an attentive human sees the printed and the missing Setup Complete! banner), but the wrong exit code still lies to any non-interactive caller: CI, && chains, wrappers, automation.

Change (uniform across all 9)

  • main() returns True on success / --clear, False on every error path.
  • __main__ captures the result and sys.exit(1) on KeyboardInterrupt, on exception, or when main() is falsy; sys.exit(0) only on real success.

Notes

  • check_enterprise_hooks_conflict() keeps its explicit raise SystemExit(3) — a deliberate, distinct exit code for "this machine is MDM-managed, defer to MDM." It's a BaseException, so it bypasses the except Exception guard and the process still exits 3 (not conflated with success=0 or generic failure=1). Untouched.
  • Added the missing import sys to claude-code/gateway, codex/gateway, and gemini-cli/gateway — their rewritten guards call sys.exit, and these three did not previously import sys.

Verification

  • python3 -m py_compile passes on all 9.
  • Zero bare return remain in any main(); every guard ends with sys.exit(0 if ok else 1).
  • Diff reviewed line-by-line: every return False is a real error path; no success path mis-flagged.
  • These installers operate on the current real user with no privilege gate (e.g. --clear wipes live config; copilot calls install_macos_certificates()), so they are intentionally not executed e2e here — validated statically + by diff review. The MDM PR (Fix silent Copilot MDM hook-install failure #171) carries the executed sandbox e2e for the equivalent contract.
  • No comments added.

Companion to #171 (MDM scripts).

🤖 Generated with Claude Code


Note

Low Risk
Behavioral change is limited to process exit codes and stricter success checks on local setup scripts; no auth or server logic changes, though CI/scripts that assumed exit 0 on failure may now see exit 1.

Overview
User-mode setup.py installers (nine tools) now report real success/failure to the shell instead of often exiting 0 after a failed run.

main() returns True on success and False on every error path (missing args, auth callback failure, env/config/hook steps). clear_setup() returns not any_failed so --clear can fail non-zero too. __main__ uses sys.exit(0 if ok else 1); KeyboardInterrupt and uncaught exceptions exit 1. MDM skip still uses SystemExit(3) unchanged.

Gateway installers (Claude, Codex, Gemini) add import sys and change Unix shell export success to require an exact matching export line in the rc file (not merely “append attempted”). Claude gateway also fails setup if ANTHROPIC_BASE_URL or setup_claude_key_helper() fails. Codex hooks now fails when enable_codex_hooks_feature() fails instead of continuing.

Reviewed by Cursor Bugbot for commit a3b7a7e. Bugbot is set up for automated code reviews on this repo. Configure here.

Greptile Summary

This PR makes the user-mode setup scripts report installer failures through process exit codes. The main changes are:

  • main() returns explicit success or failure values across the setup scripts.
  • --clear cleanup results now flow into the final exit code.
  • Top-level script guards now exit non-zero on setup failure, interruption, or exception.
  • Gateway scripts now check base URL writes and Unix rc-file read-back more strictly.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
claude-code/gateway/setup.py Propagates setup failures through exit status and checks gateway URL and key-helper writes.
gemini-cli/gateway/setup.py Propagates setup failures through exit status and checks gateway URL writes.
openclaw/setup.py Propagates cleanup and setup failures through exit status.
codex/gateway/setup.py Propagates setup, config, and cleanup failures through exit status.

Reviews (8): Last reviewed commit: "Merge branch 'fix/user-mode-setup-exit-c..." | Re-trigger Greptile

The user-mode setup.py installers (run interactively as `python3 <(curl
... setup.py) --domain ...`) had the same exit-code defect the MDM
scripts did: main() used bare `return` (-> None) on every error path and
__main__ called main() then exited 0 unless an exception was raised. So a
failed install (missing key, callback failure, env-var write, hook setup,
tool config) reported exit 0 to any non-interactive caller (CI, `&&`
chains, wrappers). An attentive human still sees the printed ❌ and the
absent "Setup Complete!", but the process exit code lied.

Mirrors the MDM exit-code contract:
- main() returns True on success / --clear, False on every error path
- __main__ captures the result and exits 1 on KeyboardInterrupt, on
  exception, or when main() is falsy; 0 only on real success

The check_enterprise_hooks_conflict() path keeps its explicit
`raise SystemExit(3)` (deliberate distinct code for "machine is
MDM-managed"); it bypasses the guard and is untouched.

Also added the missing `import sys` to claude-code/gateway,
codex/gateway, and gemini-cli/gateway (their guards now call sys.exit).

Applied to augment, claude-code (gateway + hooks), codex (gateway +
hooks), copilot, cursor, gemini-cli, and openclaw.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@MohamedAklamaash MohamedAklamaash requested a review from a team June 29, 2026 08:59
Comment thread claude-code/gateway/setup.py
Comment thread gemini-cli/gateway/setup.py
Comment thread openclaw/setup.py Outdated

@vigneshsubbiah16 vigneshsubbiah16 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛡️ Automated Security Review (consensus)

✅ Security consensus: no issues found. (reviewers: Cursor, Claude, Semgrep, Gitleaks)


🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head f5e6dad9 · 2026-06-29T09:02Z

Follow-up to PR #188 review (Greptile P1s + finder pass). The exit-code
contract was over-reporting success in several spots where a step's own
failure signal was ignored:

- claude-code/gateway, gemini-cli/gateway: the base-URL env write
  (ANTHROPIC_BASE_URL / GOOGLE_GEMINI_BASE_URL) result was captured but
  never checked, so main() reached `return True` even when the tool was
  left pointed at the wrong endpoint. Now returns False on failure,
  matching the API-key check directly above. (Greptile P1)
- openclaw + all other tools: clear_setup() tracked `any_failed`
  internally but returned None, while main()'s --clear branch returned
  True unconditionally — a partial clear reported exit 0. clear_setup()
  now returns `not any_failed` and main() propagates it via
  `return clear_setup()`. Applied uniformly to all 9 scripts (Greptile
  flagged openclaw; the same bug was systemic). (Greptile P1 + finder)
- codex/hooks: enable_codex_hooks_feature() returns bool but its result
  was ignored; a failed feature-flag write left the hooks inert while
  setup exited 0. Now returns False on failure. (finder)

Not changed: setup_claude_key_helper() and write_unbound_config() remain
best-effort (void / warn-only by design) — noted on the PR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@MohamedAklamaash

Copy link
Copy Markdown
Contributor Author

Review addressed — pushed in e325ec4

All three Greptile P1s are fixed (replied inline). A focused review pass on the same risk class (operations whose failure is ignored but now reach return True) surfaced one more, also fixed:

  • codex/hooksenable_codex_hooks_feature() returns bool but its result was ignored. A failed feature-flag write left the Codex hooks inert while setup exited 0. Now returns False (exit 1) on failure.

Deliberately left as best-effort (not failures):

  • setup_claude_key_helper() (claude-code/gateway) and write_unbound_config() — these are void / warn-only by design (they print a ⚠️ and continue). Converting them to hard failures means changing their contracts, which is out of scope for this exit-code PR. Flagging here for visibility; happy to tighten in a follow-up if desired.
  • restart_cursor() and openclaw's best-effort npm plugin install (already warns with a manual-install hint) — cosmetic / non-fatal.

Verification: py_compile passes on all 9; every main() has zero bare returns and ends wired to sys.exit(0 if ok else 1); every clear_setup() now returns not any_failed and is propagated via return clear_setup(); no stale clear_setup(); return True remains.

Comment thread claude-code/gateway/setup.py
Comment thread gemini-cli/gateway/setup.py

@vigneshsubbiah16 vigneshsubbiah16 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛡️ Automated Security Review (consensus)

✅ Security consensus: no issues found. (reviewers: Cursor, Claude, Semgrep, Gitleaks)


🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head e325ec43 · 2026-06-29T09:21Z

Review follow-up (Greptile P1 "Unix Failures Still Succeed"): the gateway
scripts' exit-code guards (`if not success: return False`) could not catch
Unix write failures because set_env_var_on_unix() returned True
unconditionally — `append_to_file()` returns False for BOTH "already
present" and "write failed", and the helper did `if was_added: return True
else: return True`, swallowing real failures.

Fix: after the append attempt, verify the export line is actually present
in the rc file. Returns True when added or already present, False when the
write failed (line absent) or the file is unreadable. No regression on the
idempotent already-present case. Applied to the 3 Pattern-A scripts
(claude-code/gateway, codex/gateway, gemini-cli/gateway); the 6 hooks
scripts already propagate failure correctly via `return append_to_file(...)`.

Verified with a temp-HOME sandbox: a read-only rc file now yields False
(was True); a writable rc file yields True with the line present.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@vigneshsubbiah16 vigneshsubbiah16 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Security consensus: no issues found. (reviewers: Cursor, Claude, Semgrep, Gitleaks)


🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head 93c44210 · 2026-06-29T09:33Z

Self-review of PR #188 surfaced two issues:

- set_env_var_on_unix() read-back caught only OSError, but
  rc_file.read_text(encoding="utf-8") raises UnicodeDecodeError (a
  ValueError) on a non-UTF-8 shell rc file — escaping the `-> bool`
  contract and bubbling a generic traceback to __main__ instead of a
  clean `return False`. Broadened to `except Exception`. (3 gateway
  scripts)
- claude-code/gateway setup_claude_key_helper() was `-> None` and
  swallowed all errors with a warning, while main() called it unchecked
  and returned True. It writes ~/.claude/anthropic_key.sh + the
  apiKeyHelper setting — the gateway auth mechanism — so a failed write
  left Claude Code unable to authenticate while setup exited 0. Now
  returns bool and main() returns False on failure, consistent with how
  the MDM script treats its managed-settings write.

Verified with temp-HOME sandbox: non-UTF-8 rc file -> set_env_var_on_unix
returns False (no exception); key helper returns True on success / False
when the write fails.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@MohamedAklamaash

Copy link
Copy Markdown
Contributor Author

Self-review (3-angle pass) — 2 issues found & fixed in 2c95425

Ran a line-by-line / removed-behavior / cross-file review over the full diff. Verified-safe: ok is never unbound in __main__ (both except arms sys.exit(1)), SystemExit(3) MDM-skip still exits 3, clear_setup() polarity (return not any_failed, not_found excluded) is correct, Windows set_env_var path unchanged, idempotent re-runs still succeed, and module-importing tests are unaffected (main() itself never calls sys.exit).

Fixed:

  1. set_env_var_on_unix() read-back caught only OSError (3 gateway scripts). rc_file.read_text(encoding="utf-8") raises UnicodeDecodeError (a ValueError) on a non-UTF-8 shell rc file, which escaped the -> bool contract and bubbled a generic traceback to __main__ instead of a clean return False. Broadened to except Exception.
  2. setup_claude_key_helper() failure was silent (claude-code/gateway). It was -> None, swallowed errors with a ⚠️ warning, and main() called it unchecked → return True. It writes ~/.claude/anthropic_key.sh + the apiKeyHelper setting (the gateway auth mechanism), so a failed write left Claude Code unable to authenticate while setup exited 0. Now returns bool and main() returns False on failure — consistent with how the MDM script treats its managed-settings write.

Noted, intentionally not changed (best-effort by design): write_unbound_config() (Dock/Spotlight env convenience; copilot/cursor only warn), run_backfill() (post-setup, -> None, swallows its own errors), and the pre-existing lack of var_name dedup in the gateway append_to_file (duplicate export lines accumulate on URL change; shell uses the last).

Verified with a temp-HOME sandbox: non-UTF-8 rc → set_env_var_on_unix returns False (no exception); key helper returns True on success / False when the write fails.

@vigneshsubbiah16

Copy link
Copy Markdown
Collaborator

✅ Security consensus: no issues found. (reviewers: Cursor, Claude, Semgrep, Gitleaks)


🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head 2c954254 · 2026-06-29T10:21Z

@vigneshsubbiah16

Copy link
Copy Markdown
Collaborator

🛡️ Automated Security Review (consensus)

0 findings — 0 high-confidence, 0 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks.

✅ Security consensus: no issues found. (reviewers: Cursor, Claude, Semgrep, Gitleaks)

Previously acknowledged (not re-flagged)

  • write_unbound_config() / run_backfill() best-effort — Maintainer: deliberately warn-only / post-setup; hard-failing would change existing contracts; out of scope for this exit-code PR.
  • restart_cursor() / OpenClaw npm plugin install — Maintainer: cosmetic / non-fatal; setup continues with manual-install guidance.
  • Gateway append_to_file duplicate export lines — Maintainer: pre-existing behavior; intentionally not changed in this PR.
  • Greptile P1s (base-URL write, Unix rc read-back, --clear propagation, codex feature-flag, key-helper) — Fixed in e325ec4 / 93c4421 / 2c95425; not re-raised.
  • Semgrep insecure-file-permissions on 0o700 — Pre-existing, unchanged; 0o700 is owner-only (stricter than 0o644); rule misfire.
  • Semgrep subprocess-shell-true (cursor/setup.py:423) — Pre-existing Popen(..., shell=True); untouched by this diff.

🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head 541d3cc3 · 2026-07-01T13:52Z

Comment thread claude-code/gateway/setup.py Outdated
Comment thread gemini-cli/gateway/setup.py Outdated
@MohamedAklamaash MohamedAklamaash changed the base branch from main to staging July 1, 2026 15:05
Greptile P1 "Commented Export Succeeds": the read-back
`export_line in rc_file.read_text()` did a substring match, so a disabled
line like `# export ANTHROPIC_BASE_URL="..."` satisfied it — set_env_var
could report success (exit 0) without an active export, leaving the tool
on the old/default endpoint.

Now require an exact, stripped, uncommented line match:
`any(line.strip() == export_line for line in ...splitlines())`. A
`# export VAR=...` line no longer counts; the real appended export still
does. Keeps the broad `except Exception: return False` (non-UTF-8 rc).
Applied to all 3 gateway scripts (Greptile flagged claude-code + gemini;
codex/gateway has the identical read-back).

Verified in a temp-HOME sandbox: commented-only rc -> False, active
export -> True, non-UTF-8 rc -> False (no exception).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@vigneshsubbiah16 vigneshsubbiah16 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Security consensus: no issues found. (reviewers: Cursor, Claude, Semgrep, Gitleaks)


🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head b2aa2248 · 2026-07-01T15:20Z

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 6f6aa58. Configure here.

try:
return any(line.strip() == export_line for line in rc_file.read_text(encoding="utf-8").splitlines())
except Exception:
return False

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commented export blocks gateway setup

Medium Severity

On Unix, gateway setup now verifies an exact active export line in the shell rc file, but append_to_file still skips writing when that text appears anywhere in the file (including inside a commented # export … line). With only a commented export present, no active line is added, read-back fails, and setup exits with failure even though the installer never writes a usable export.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6f6aa58. Configure here.

@vigneshsubbiah16

Copy link
Copy Markdown
Collaborator

🛡️ Automated Security Review (consensus)

0 findings — 0 high-confidence, 0 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks.

✅ Security consensus: no issues found. (reviewers: Cursor, Claude, Semgrep, Gitleaks)

Previously acknowledged (not re-flagged)

  • Best-effort write_unbound_config() / run_backfill() / restart_cursor() / OpenClaw npm install — Maintainer: warn-only / cosmetic by design; tightening their contracts is out of scope for this exit-code PR.
  • Semgrep insecure-file-permissions (0o700 on hook/install paths) — Pre-existing owner-only modes on installer artifacts; Semgrep mislabels restrictive 0700 as permissive (false positive).
  • Semgrep subprocess-shell-true (cursor/setup.py) — Pre-existing Popen(..., shell=True) unchanged in this diff; prior consensus accepted for this PR’s scope.
  • Gateway env-write / clear_setup() / Unix read-back / commented-export threads — Fixed in e325ec4, 93c4421, b2aa224, 2c95425 per maintainer replies; not re-raised.
  • Cursor Bugbot “commented export blocks setup” — Fail-closed functional behavior (exit 1 when no active export); not a security regression.

🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head 6f6aa588 · 2026-07-02T16:02Z

@MohamedAklamaash MohamedAklamaash merged commit dfdebe5 into staging Jul 3, 2026
4 checks passed

@vigneshsubbiah16 vigneshsubbiah16 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛡️ Automated Security Review (consensus)

0 findings — 0 high-confidence, 0 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks.

✅ Security consensus: no issues found. (reviewers: Cursor, Claude, Semgrep, Gitleaks)

Previously acknowledged (not re-flagged)

  • write_unbound_config() / run_backfill() warn-only paths — Maintainer: best-effort by design; hard-failing would change their contracts and is out of scope for this exit-code PR.
  • restart_cursor() (subprocess.Popen(..., shell=True)) and openclaw npm plugin install — Maintainer: cosmetic / non-fatal best-effort; Semgrep shell=True hit is pre-existing code on an intentionally non-blocking path.
  • Gateway append_to_file lacks var_name dedup (duplicate export lines on URL change) — Maintainer: pre-existing behavior, accepted; shell uses the last export.
  • Commented # export … in rc file causes append_to_file to skip write → setup exits 1 — Fails closed (no false “enrolled” signal); correctness/availability nuisance, not a security exposure. Greptile read-back issues and related P1s were fixed in e325ec4 / 93c4421 / b2aa224.

🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head a3b7a7ee · 2026-07-03T06:21Z

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants