From f5e6dad9a79adce89e45e317821bc4b41d665b24 Mon Sep 17 00:00:00 2001 From: MohamedAklamaash Date: Mon, 29 Jun 2026 14:28:37 +0530 Subject: [PATCH 1/5] Propagate user-mode setup failures via exit code across all tools MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- augment/hooks/setup.py | 22 +++++++++++++--------- claude-code/gateway/setup.py | 19 ++++++++++++------- claude-code/hooks/setup.py | 21 ++++++++++++--------- codex/gateway/setup.py | 20 ++++++++++++-------- codex/hooks/setup.py | 22 +++++++++++++--------- copilot/hooks/setup.py | 22 +++++++++++++--------- cursor/setup.py | 19 +++++++++++-------- gemini-cli/gateway/setup.py | 19 ++++++++++++------- openclaw/setup.py | 18 +++++++++++------- 9 files changed, 109 insertions(+), 73 deletions(-) diff --git a/augment/hooks/setup.py b/augment/hooks/setup.py index 851647de..a202d574 100644 --- a/augment/hooks/setup.py +++ b/augment/hooks/setup.py @@ -904,7 +904,7 @@ def main(): if clear_mode: clear_setup() - return + return True if check_enterprise_hooks_conflict(): print("\n❌ Skipped — Augment is managed by your organization (MDM).") @@ -944,14 +944,14 @@ def main(): if not api_key: if not domain: print("❌ Missing required argument: --domain or --api-key") - return + return False auth_url = normalize_url(domain) cb_response = run_callback_server(auth_url) if cb_response is None: print("❌ Failed to receive callback. Exiting.") - return + return False try: api_key = (cb_response.get("query") or {}).get("api_key") @@ -965,7 +965,7 @@ def main(): print(f"❌ Setup failed: {safe_error}") else: print("❌ No API key received. Exiting.") - return + return False debug_print("API key received from callback") @@ -973,7 +973,7 @@ def main(): success, message = set_env_var("UNBOUND_AUGMENT_API_KEY", api_key) if not success: print(f"❌ Failed to set environment variable: {message}") - return + return False debug_print("UNBOUND_AUGMENT_API_KEY set successfully") _install_state = detect_install_state() @@ -984,13 +984,13 @@ def main(): debug_print("Setting up hooks...") if not setup_hooks(gateway_url=gateway_url): print("❌ Failed to setup hooks") - return + return False debug_print("Hooks downloaded successfully") debug_print("Configuring Augment settings...") if not configure_augment_settings(): print("❌ Failed to configure Augment settings") - return + return False debug_print("Augment settings configured successfully") print("✅ API key verified and added") @@ -1003,12 +1003,16 @@ def main(): if rc_path is not None: print(f"\nTo apply changes in your current terminal, run:\n source {rc_path}\n\nOr open a new terminal.") + return True + if __name__ == "__main__": try: - main() + ok = main() except KeyboardInterrupt: print("\n\n⚠️ Setup cancelled.") + sys.exit(1) except Exception as e: print(f"\n❌ Error: {e}") - exit(1) + sys.exit(1) + sys.exit(0 if ok else 1) diff --git a/claude-code/gateway/setup.py b/claude-code/gateway/setup.py index 33b56f60..0a10d162 100644 --- a/claude-code/gateway/setup.py +++ b/claude-code/gateway/setup.py @@ -4,6 +4,7 @@ """ import os +import sys import platform import subprocess import urllib.request @@ -676,7 +677,7 @@ def main(): if args.clear: clear_setup() - return + return True if check_enterprise_hooks_conflict(): print("\n❌ Skipped — Claude Code is managed by your organization (MDM).") @@ -704,13 +705,13 @@ def main(): if not api_key: if not args.domain: print("\n❌ Missing required argument: --domain or --api-key") - return + return False auth_url = normalize_url(args.domain) cb_response = run_one_shot_callback_server(auth_url) if cb_response is None: print("\n❌ Failed to receive callback response. Exiting.") - return + return False try: api_key = (cb_response.get("query") or {}).get("api_key") @@ -719,7 +720,7 @@ def main(): if not api_key: print("\n❌ No api_key found in callback. Exiting.") - return + return False print("API Key Verified ✅") debug_print("API key verification successful") @@ -728,7 +729,7 @@ def main(): success, message = set_env_var("UNBOUND_API_KEY", api_key) if not success: print(f"❌ Failed to configure UNBOUND_API_KEY: {message}") - return + return False debug_print("UNBOUND_API_KEY set successfully") debug_print("Setting ANTHROPIC_BASE_URL environment variable...") @@ -756,11 +757,15 @@ def main(): if rc_path is not None: print(f"\nTo apply changes in your current terminal, run:\n source {rc_path}\n\nOr open a new terminal.") + return True + if __name__ == "__main__": try: - main() + ok = main() except KeyboardInterrupt: print("\n\n⚠️ Setup cancelled by user.") + sys.exit(1) except Exception as e: print(f"\n❌ An error occurred: {e}") - exit(1) + sys.exit(1) + sys.exit(0 if ok else 1) diff --git a/claude-code/hooks/setup.py b/claude-code/hooks/setup.py index a3bce638..9de54fd4 100644 --- a/claude-code/hooks/setup.py +++ b/claude-code/hooks/setup.py @@ -1198,7 +1198,7 @@ def main(): if clear_mode: clear_setup() - return + return True if check_enterprise_hooks_conflict(): print("\n❌ Skipped — Claude Code is managed by your organization (MDM).") @@ -1238,14 +1238,14 @@ def main(): if not api_key: if not domain: print("❌ Missing required argument: --domain or --api-key") - return + return False auth_url = normalize_url(domain) cb_response = run_callback_server(auth_url) if cb_response is None: print("❌ Failed to receive callback. Exiting.") - return + return False try: api_key = (cb_response.get("query") or {}).get("api_key") @@ -1259,7 +1259,7 @@ def main(): print(f"❌ Setup failed: {safe_error}") else: print("❌ No API key received. Exiting.") - return + return False debug_print("API key received from callback") @@ -1275,7 +1275,7 @@ def main(): success, message = set_env_var("UNBOUND_CLAUDE_API_KEY", api_key) if not success: print(f"❌ Failed to set environment variable: {message}") - return + return False debug_print("UNBOUND_CLAUDE_API_KEY set successfully") _install_state = detect_install_state() @@ -1286,13 +1286,13 @@ def main(): debug_print("Setting up hooks...") if not setup_hooks(gateway_url=gateway_url): print("❌ Failed to setup hooks") - return + return False debug_print("Hooks downloaded successfully") debug_print("Configuring Claude settings...") if not configure_claude_settings(): print("❌ Failed to configure Claude settings") - return + return False debug_print("Claude settings configured successfully") print("✅ API key verified and added") @@ -1307,13 +1307,16 @@ def main(): rc_path = get_shell_rc_file() if rc_path is not None: print(f"\nTo apply changes in your current terminal, run:\n source {rc_path}\n\nOr open a new terminal.") + return True if __name__ == "__main__": try: - main() + ok = main() except KeyboardInterrupt: print("\n\n⚠️ Setup cancelled.") + sys.exit(1) except Exception as e: print(f"\n❌ Error: {e}") - exit(1) \ No newline at end of file + sys.exit(1) + sys.exit(0 if ok else 1) \ No newline at end of file diff --git a/codex/gateway/setup.py b/codex/gateway/setup.py index f459fc55..f8b020e7 100644 --- a/codex/gateway/setup.py +++ b/codex/gateway/setup.py @@ -4,6 +4,7 @@ """ import os +import sys import platform import subprocess import urllib.request @@ -729,7 +730,7 @@ def main(): if args.clear: clear_setup() - return + return True if check_enterprise_hooks_conflict(): print("\n❌ Skipped — Codex is managed by your organization (MDM).") @@ -743,13 +744,13 @@ def main(): if not api_key: if not args.domain: print("\n❌ Missing required argument: --domain or --api-key") - return + return False auth_url = normalize_url(args.domain) cb_response = run_one_shot_callback_server(auth_url) if cb_response is None: print("\n❌ Failed to receive callback response. Exiting.") - return + return False try: api_key = (cb_response.get("query") or {}).get("api_key") @@ -758,7 +759,7 @@ def main(): if not api_key: print("\n❌ No api_key found in callback. Exiting.") - return + return False print("API Key Verified ✅") debug_print("API key verification successful") @@ -779,13 +780,13 @@ def main(): success, message = set_env_var("OPENAI_API_KEY", api_key) if not success: print(f"❌ Failed to configure OPENAI_API_KEY: {message}") - return + return False debug_print("OPENAI_API_KEY set successfully") debug_print("Writing openai_base_url to codex config...") if not write_codex_config(f"{args.gateway_url.rstrip('/')}/v1"): print("❌ Failed to configure openai_base_url in codex config") - return + return False debug_print("openai_base_url written to codex config successfully") write_unbound_config(api_key, urls={"base_url": args.backend_url, "gateway_url": args.gateway_url, "frontend_url": normalize_url(args.domain) if args.domain else None}) @@ -800,12 +801,15 @@ def main(): rc_path = get_shell_rc_file() if rc_path is not None: print(f"\nTo apply changes in your current terminal, run:\n source {rc_path}\n\nOr open a new terminal.") + return True if __name__ == "__main__": try: - main() + ok = main() except KeyboardInterrupt: print("\n\n⚠️ Setup cancelled by user.") + sys.exit(1) except Exception as e: print(f"\n❌ An error occurred: {e}") - exit(1) + sys.exit(1) + sys.exit(0 if ok else 1) diff --git a/codex/hooks/setup.py b/codex/hooks/setup.py index 4f8f4467..7a3c934f 100644 --- a/codex/hooks/setup.py +++ b/codex/hooks/setup.py @@ -1288,7 +1288,7 @@ def main(): if clear_mode: clear_setup() - return + return True if check_enterprise_hooks_conflict(): print("\n❌ Skipped — Codex is managed by your organization (MDM).") @@ -1328,14 +1328,14 @@ def main(): if not api_key: if not domain: print("Missing required argument: --domain or --api-key") - return + return False auth_url = normalize_url(domain) cb_response = run_callback_server(auth_url) if cb_response is None: print("Failed to receive callback. Exiting.") - return + return False try: api_key = (cb_response.get("query") or {}).get("api_key") @@ -1349,7 +1349,7 @@ def main(): print(f"Setup failed: {safe_error}") else: print("No API key received. Exiting.") - return + return False debug_print("API key received from callback") @@ -1363,7 +1363,7 @@ def main(): success, message = set_env_var("UNBOUND_CODEX_API_KEY", api_key) if not success: print(f"Failed to set environment variable: {message}") - return + return False debug_print("UNBOUND_CODEX_API_KEY set successfully") write_unbound_config(api_key, urls={"base_url": backend_url, "gateway_url": gateway_url, "frontend_url": normalize_url(domain) if domain else None}) @@ -1371,13 +1371,13 @@ def main(): debug_print("Setting up hooks...") if not setup_hooks(gateway_url=gateway_url): print("Failed to setup hooks") - return + return False debug_print("Hooks downloaded successfully") debug_print("Configuring Codex hooks...") if not configure_codex_hooks(): print("Failed to configure Codex hooks") - return + return False debug_print("Codex hooks configured successfully") debug_print("Enabling codex_hooks feature flag...") @@ -1396,12 +1396,16 @@ def main(): if rc_path is not None: print(f"\nTo apply changes in your current terminal, run:\n source {rc_path}\n\nOr open a new terminal.") + return True + if __name__ == "__main__": try: - main() + ok = main() except KeyboardInterrupt: print("\n\nSetup cancelled.") + sys.exit(1) except Exception as e: print(f"\nError: {e}") - exit(1) + sys.exit(1) + sys.exit(0 if ok else 1) diff --git a/copilot/hooks/setup.py b/copilot/hooks/setup.py index bccdbc81..4430f6c1 100644 --- a/copilot/hooks/setup.py +++ b/copilot/hooks/setup.py @@ -988,7 +988,7 @@ def main(): if clear_mode: clear_setup() - return + return True install_macos_certificates() @@ -1024,14 +1024,14 @@ def main(): if not api_key: if not domain: print("\n❌ Missing required argument: --domain or --api-key") - return + return False auth_url = normalize_url(domain) cb_response = run_callback_server(auth_url) if cb_response is None: print("\n❌ Failed to receive callback. Exiting.") - return + return False try: api_key = (cb_response.get("query") or {}).get("api_key") @@ -1040,7 +1040,7 @@ def main(): if not api_key: print("\n❌ No API key received. Exiting.") - return + return False print("✅ API key received") debug_print("API key received from callback") @@ -1055,7 +1055,7 @@ def main(): success, message = set_env_var("UNBOUND_COPILOT_API_KEY", api_key) if not success: print(f"❌ Failed to set environment variable: {message}") - return + return False print(f"✅ Environment variable set") debug_print("UNBOUND_COPILOT_API_KEY set successfully") @@ -1063,13 +1063,13 @@ def main(): debug_print("Setting up hooks...") if not setup_hooks(gateway_url=gateway_url): print("\n❌ Failed to setup hooks") - return + return False debug_print("Hooks setup complete") debug_print("Configuring Copilot hooks...") if not configure_copilot_hooks(): print("\n❌ Failed to configure Copilot hooks") - return + return False debug_print("Copilot hooks configured") print("\n" + "=" * 60) @@ -1085,12 +1085,16 @@ def main(): if rc_path is not None: print(f"\nTo apply changes in your current terminal, run:\n source {rc_path}\n\nOr open a new terminal.") + return True + if __name__ == "__main__": try: - main() + ok = main() except KeyboardInterrupt: print("\n\n⚠️ Setup cancelled.") + sys.exit(1) except Exception as e: print(f"\n❌ Error: {e}") - exit(1) + sys.exit(1) + sys.exit(0 if ok else 1) diff --git a/cursor/setup.py b/cursor/setup.py index 0c47fea5..23ffd40f 100644 --- a/cursor/setup.py +++ b/cursor/setup.py @@ -643,7 +643,7 @@ def main(): if clear_mode: clear_setup() - return + return True if check_enterprise_hooks_conflict(): print("\n❌ Skipped — Cursor is managed by your organization (MDM).") @@ -683,14 +683,14 @@ def main(): if not api_key: if not domain: print("\n❌ Missing required argument: --domain or --api-key") - return + return False auth_url = normalize_url(domain) cb_response = run_callback_server(auth_url) if cb_response is None: print("\n❌ Failed to receive callback. Exiting.") - return + return False try: api_key = (cb_response.get("query") or {}).get("api_key") @@ -699,7 +699,7 @@ def main(): if not api_key: print("\n❌ No API key received. Exiting.") - return + return False print("✅ API key received") debug_print("API key received from callback") @@ -714,7 +714,7 @@ def main(): success, message = set_env_var("UNBOUND_CURSOR_API_KEY", api_key) if not success: print(f"❌ Failed to set environment variable: {message}") - return + return False print(f"✅ Environment variable set") debug_print("UNBOUND_CURSOR_API_KEY set successfully") @@ -722,7 +722,7 @@ def main(): debug_print("Setting up hooks...") if not setup_hooks(gateway_url=gateway_url): print("\n❌ Failed to setup hooks") - return + return False debug_print("Hooks setup complete") print("\n" + "=" * 60) @@ -736,13 +736,16 @@ def main(): rc_path = get_shell_rc_file() if rc_path is not None: print(f"\nTo apply changes in your current terminal, run:\n source {rc_path}\n\nOr open a new terminal.") + return True if __name__ == "__main__": try: - main() + ok = main() except KeyboardInterrupt: print("\n\n⚠️ Setup cancelled.") + sys.exit(1) except Exception as e: print(f"\n❌ Error: {e}") - exit(1) \ No newline at end of file + sys.exit(1) + sys.exit(0 if ok else 1) \ No newline at end of file diff --git a/gemini-cli/gateway/setup.py b/gemini-cli/gateway/setup.py index c6fa88ff..0706d979 100644 --- a/gemini-cli/gateway/setup.py +++ b/gemini-cli/gateway/setup.py @@ -4,6 +4,7 @@ """ import os +import sys import platform import subprocess import urllib.request @@ -549,7 +550,7 @@ def main(): if args.clear: clear_setup() - return + return True print("=" * 60) print("Gemini CLI - Environment Setup") @@ -559,13 +560,13 @@ def main(): if not api_key: if not args.domain: print("\n❌ Missing required argument: --domain or --api-key") - return + return False auth_url = normalize_url(args.domain) cb_response = run_one_shot_callback_server(auth_url) if cb_response is None: print("\n❌ Failed to receive callback response. Exiting.") - return + return False try: api_key = (cb_response.get("query") or {}).get("api_key") @@ -574,7 +575,7 @@ def main(): if not api_key: print("\n❌ No api_key found in callback. Exiting.") - return + return False print("API Key Verified ✅") debug_print("API key verification successful") @@ -583,7 +584,7 @@ def main(): success, message = set_env_var("GEMINI_API_KEY", api_key) if not success: print(f"❌ Failed to configure GEMINI_API_KEY: {message}") - return + return False debug_print("GEMINI_API_KEY set successfully") debug_print("Setting GOOGLE_GEMINI_BASE_URL environment variable...") @@ -606,11 +607,15 @@ def main(): if rc_path is not None: print(f"\nTo apply changes in your current terminal, run:\n source {rc_path}\n\nOr open a new terminal.") + return True + if __name__ == "__main__": try: - main() + ok = main() except KeyboardInterrupt: print("\n\n⚠️ Setup cancelled by user.") + sys.exit(1) except Exception as e: print(f"\n❌ An error occurred: {e}") - exit(1) + sys.exit(1) + sys.exit(0 if ok else 1) diff --git a/openclaw/setup.py b/openclaw/setup.py index 71c4851e..8c054c61 100644 --- a/openclaw/setup.py +++ b/openclaw/setup.py @@ -496,7 +496,7 @@ def main(): if clear_mode: clear_setup() - return + return True install_macos_certificates() @@ -532,14 +532,14 @@ def main(): if not api_key: if not domain: print("❌ Missing required argument: --domain or --api-key") - return + return False auth_url = normalize_url(domain) api_key = run_callback_server(auth_url) if not api_key: print("❌ No API key received. Exiting.") - return + return False debug_print("API key received from callback") @@ -548,13 +548,13 @@ def main(): success, message = set_env_var(ENV_VAR_NAME, api_key) if not success: print(f"❌ Failed to set environment variable: {message}") - return + return False # Configure OpenClaw debug_print("Configuring OpenClaw...") if not configure_openclaw(gateway_url, setup_plugin=setup_plugin, setup_provider=setup_provider, model=model): print("❌ Failed to configure OpenClaw") - return + return False print("✅ API key verified and added") if setup_plugin: @@ -575,12 +575,16 @@ def main(): if rc_path is not None: print(f"\nTo apply changes in your current terminal, run:\n source {rc_path}\n\nOr open a new terminal.") + return True + if __name__ == "__main__": try: - main() + ok = main() except KeyboardInterrupt: print("\n\n⚠️ Setup cancelled.") + sys.exit(1) except Exception as e: print(f"\n❌ Error: {e}") - exit(1) + sys.exit(1) + sys.exit(0 if ok else 1) From e325ec43168d6956ec0263cea74ad3bd5a3ef07b Mon Sep 17 00:00:00 2001 From: MohamedAklamaash Date: Mon, 29 Jun 2026 14:45:45 +0530 Subject: [PATCH 2/5] Address review: don't report success when a checked step fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- augment/hooks/setup.py | 7 ++++--- claude-code/gateway/setup.py | 10 +++++++--- claude-code/hooks/setup.py | 6 +++--- codex/gateway/setup.py | 6 +++--- codex/hooks/setup.py | 10 ++++++---- copilot/hooks/setup.py | 7 ++++--- cursor/setup.py | 6 +++--- gemini-cli/gateway/setup.py | 10 +++++++--- openclaw/setup.py | 6 +++--- 9 files changed, 40 insertions(+), 28 deletions(-) diff --git a/augment/hooks/setup.py b/augment/hooks/setup.py index a202d574..027fb56f 100644 --- a/augment/hooks/setup.py +++ b/augment/hooks/setup.py @@ -644,7 +644,7 @@ def _clear_path(path: Path, label: str) -> str: return "failed" -def clear_setup() -> None: +def clear_setup() -> bool: """Undo all changes made by the setup script.""" print("=" * 60) print("Augment Code Hooks - Clearing Setup") @@ -692,6 +692,8 @@ def clear_setup() -> None: print("Clear Complete!") print("=" * 60) + return not any_failed + def get_device_identifier() -> Optional[str]: system = platform.system().lower() @@ -903,8 +905,7 @@ def main(): print("[backfill] Augment backfill is not supported.") if clear_mode: - clear_setup() - return True + return clear_setup() if check_enterprise_hooks_conflict(): print("\n❌ Skipped — Augment is managed by your organization (MDM).") diff --git a/claude-code/gateway/setup.py b/claude-code/gateway/setup.py index 0a10d162..5e3bbfdd 100644 --- a/claude-code/gateway/setup.py +++ b/claude-code/gateway/setup.py @@ -449,7 +449,7 @@ def remove_api_key_helper_setting() -> str: return "failed" -def clear_setup() -> None: +def clear_setup() -> bool: """Undo all changes made by the setup script.""" print("=" * 60) print("Claude Code - Clearing Setup") @@ -488,6 +488,8 @@ def clear_setup() -> None: print("Clear Complete!") print("=" * 60) + return not any_failed + def get_device_identifier() -> Optional[str]: system = platform.system().lower() @@ -676,8 +678,7 @@ def main(): debug_print("Debug mode enabled") if args.clear: - clear_setup() - return True + return clear_setup() if check_enterprise_hooks_conflict(): print("\n❌ Skipped — Claude Code is managed by your organization (MDM).") @@ -734,6 +735,9 @@ def main(): debug_print("Setting ANTHROPIC_BASE_URL environment variable...") success, message = set_env_var("ANTHROPIC_BASE_URL", args.gateway_url) + if not success: + print(f"❌ Failed to configure ANTHROPIC_BASE_URL: {message}") + return False debug_print("ANTHROPIC_BASE_URL set successfully") _install_state = detect_install_state() diff --git a/claude-code/hooks/setup.py b/claude-code/hooks/setup.py index 9de54fd4..bfe5d75d 100644 --- a/claude-code/hooks/setup.py +++ b/claude-code/hooks/setup.py @@ -612,7 +612,7 @@ def _clear_path(path: Path, label: str) -> str: return "failed" -def clear_setup() -> None: +def clear_setup() -> bool: """Undo all changes made by the setup script.""" print("=" * 60) print("Claude Code Hooks - Clearing Setup") @@ -659,6 +659,7 @@ def clear_setup() -> None: print("\n" + "=" * 60) print("Clear Complete!") print("=" * 60) + return not any_failed def get_device_identifier() -> Optional[str]: @@ -1197,8 +1198,7 @@ def main(): debug_print("Debug mode enabled") if clear_mode: - clear_setup() - return True + return clear_setup() if check_enterprise_hooks_conflict(): print("\n❌ Skipped — Claude Code is managed by your organization (MDM).") diff --git a/codex/gateway/setup.py b/codex/gateway/setup.py index f8b020e7..215af0ce 100644 --- a/codex/gateway/setup.py +++ b/codex/gateway/setup.py @@ -457,7 +457,7 @@ def remove_codex_config_base_url() -> str: -def clear_setup() -> None: +def clear_setup() -> bool: """Undo all changes made by the setup script.""" global DEBUG print("=" * 60) @@ -490,6 +490,7 @@ def clear_setup() -> None: print("\n" + "=" * 60) print("Clear Complete!") print("=" * 60) + return not any_failed def remove_hooks_unbound_script() -> None: @@ -729,8 +730,7 @@ def main(): debug_print("Debug mode enabled") if args.clear: - clear_setup() - return True + return clear_setup() if check_enterprise_hooks_conflict(): print("\n❌ Skipped — Codex is managed by your organization (MDM).") diff --git a/codex/hooks/setup.py b/codex/hooks/setup.py index 7a3c934f..6737be0b 100644 --- a/codex/hooks/setup.py +++ b/codex/hooks/setup.py @@ -618,7 +618,7 @@ def _clear_path(path: Path, label: str) -> str: return "failed" -def clear_setup() -> None: +def clear_setup() -> bool: """Undo all changes made by the setup script.""" print("=" * 60) print("Codex Hooks - Clearing Setup") @@ -672,6 +672,7 @@ def clear_setup() -> None: print("\n" + "=" * 60) print("Clear Complete!") print("=" * 60) + return not any_failed def enable_codex_hooks_feature() -> bool: @@ -1287,8 +1288,7 @@ def main(): debug_print("Debug mode enabled") if clear_mode: - clear_setup() - return True + return clear_setup() if check_enterprise_hooks_conflict(): print("\n❌ Skipped — Codex is managed by your organization (MDM).") @@ -1381,7 +1381,9 @@ def main(): debug_print("Codex hooks configured successfully") debug_print("Enabling codex_hooks feature flag...") - enable_codex_hooks_feature() + if not enable_codex_hooks_feature(): + print("Failed to enable codex_hooks feature flag") + return False print("API key verified and added") print("Setup complete") diff --git a/copilot/hooks/setup.py b/copilot/hooks/setup.py index 4430f6c1..f51367c8 100644 --- a/copilot/hooks/setup.py +++ b/copilot/hooks/setup.py @@ -422,7 +422,7 @@ def _clear_path(path: Path, label: str) -> str: return "failed" -def clear_setup() -> None: +def clear_setup() -> bool: """Undo all changes made by the setup script.""" print("=" * 60) print("Unbound Copilot Hooks - Clearing Setup") @@ -458,6 +458,8 @@ def clear_setup() -> None: print("Clear Complete!") print("=" * 60) + return not any_failed + def get_device_identifier() -> Optional[str]: system = platform.system().lower() @@ -987,8 +989,7 @@ def main(): debug_print("Debug mode enabled") if clear_mode: - clear_setup() - return True + return clear_setup() install_macos_certificates() diff --git a/cursor/setup.py b/cursor/setup.py index 23ffd40f..dd230741 100644 --- a/cursor/setup.py +++ b/cursor/setup.py @@ -455,7 +455,7 @@ def _clear_path(path: Path, label: str) -> str: return "failed" -def clear_setup() -> None: +def clear_setup() -> bool: """Undo all changes made by the setup script.""" print("=" * 60) print("Unbound Cursor Hooks - Clearing Setup") @@ -490,6 +490,7 @@ def clear_setup() -> None: print("\n" + "=" * 60) print("Clear Complete!") print("=" * 60) + return not any_failed def get_device_identifier() -> Optional[str]: @@ -642,8 +643,7 @@ def main(): print("[backfill] Cursor backfill is not supported — no historical transcript data is available on disk.") if clear_mode: - clear_setup() - return True + return clear_setup() if check_enterprise_hooks_conflict(): print("\n❌ Skipped — Cursor is managed by your organization (MDM).") diff --git a/gemini-cli/gateway/setup.py b/gemini-cli/gateway/setup.py index 0706d979..29bfcd98 100644 --- a/gemini-cli/gateway/setup.py +++ b/gemini-cli/gateway/setup.py @@ -365,7 +365,7 @@ def write_unbound_config(api_key: str, urls: dict = None) -> bool: -def clear_setup() -> None: +def clear_setup() -> bool: """Undo all changes made by the setup script.""" print("=" * 60) print("Gemini CLI - Clearing Setup") @@ -391,6 +391,8 @@ def clear_setup() -> None: print("Clear Complete!") print("=" * 60) + return not any_failed + def get_device_identifier() -> Optional[str]: system = platform.system().lower() @@ -549,8 +551,7 @@ def main(): debug_print("Debug mode enabled") if args.clear: - clear_setup() - return True + return clear_setup() print("=" * 60) print("Gemini CLI - Environment Setup") @@ -589,6 +590,9 @@ def main(): debug_print("Setting GOOGLE_GEMINI_BASE_URL environment variable...") success, message = set_env_var("GOOGLE_GEMINI_BASE_URL", f"{args.gateway_url.rstrip('/')}/v1") + if not success: + print(f"❌ Failed to configure GOOGLE_GEMINI_BASE_URL: {message}") + return False debug_print("GOOGLE_GEMINI_BASE_URL set successfully") _install_state = detect_install_state() diff --git a/openclaw/setup.py b/openclaw/setup.py index 8c054c61..950a56be 100644 --- a/openclaw/setup.py +++ b/openclaw/setup.py @@ -371,7 +371,7 @@ def _report_status(status: str, label: str) -> bool: return False -def clear_setup() -> None: +def clear_setup() -> bool: """Undo all changes made by the setup script.""" print("=" * 60) print("OpenClaw Unbound Plugin - Clearing Setup") @@ -463,6 +463,7 @@ def clear_setup() -> None: print("\n" + "=" * 60) print("Clear Complete!") print("=" * 60) + return not any_failed def notify_setup_complete(api_key: str, tool_type: str, backend_url: str = "https://backend.getunbound.ai"): @@ -495,8 +496,7 @@ def main(): debug_print("Debug mode enabled") if clear_mode: - clear_setup() - return True + return clear_setup() install_macos_certificates() From 93c442105e9eb5a30ff59113f54ffe0bbcc3207a Mon Sep 17 00:00:00 2001 From: MohamedAklamaash Date: Mon, 29 Jun 2026 14:56:21 +0530 Subject: [PATCH 3/5] Make set_env_var_on_unix report rc-file write failures truthfully MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- claude-code/gateway/setup.py | 12 ++++++------ codex/gateway/setup.py | 12 ++++++------ gemini-cli/gateway/setup.py | 12 ++++++------ 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/claude-code/gateway/setup.py b/claude-code/gateway/setup.py index 5e3bbfdd..7d90907a 100644 --- a/claude-code/gateway/setup.py +++ b/claude-code/gateway/setup.py @@ -144,12 +144,12 @@ def set_env_var_on_unix(var_name: str, value: str) -> bool: debug_print(f"Writing to shell file: {rc_file}") export_line = f'export {var_name}="{value}"' - was_added = append_to_file(rc_file, export_line) - - if was_added: - return True - else: - return True + append_to_file(rc_file, export_line) + + try: + return export_line in rc_file.read_text(encoding="utf-8") + except OSError: + return False def set_env_var(var_name: str, value: str) -> Tuple[bool, str]: diff --git a/codex/gateway/setup.py b/codex/gateway/setup.py index 215af0ce..2ddb9f3b 100644 --- a/codex/gateway/setup.py +++ b/codex/gateway/setup.py @@ -144,12 +144,12 @@ def set_env_var_on_unix(var_name: str, value: str) -> bool: debug_print(f"Writing to shell file: {rc_file}") export_line = f'export {var_name}="{value}"' - was_added = append_to_file(rc_file, export_line) - - if was_added: - return True - else: - return True + append_to_file(rc_file, export_line) + + try: + return export_line in rc_file.read_text(encoding="utf-8") + except OSError: + return False def set_env_var(var_name: str, value: str) -> Tuple[bool, str]: diff --git a/gemini-cli/gateway/setup.py b/gemini-cli/gateway/setup.py index 29bfcd98..4176d5d7 100644 --- a/gemini-cli/gateway/setup.py +++ b/gemini-cli/gateway/setup.py @@ -144,12 +144,12 @@ def set_env_var_on_unix(var_name: str, value: str) -> bool: debug_print(f"Writing to shell file: {rc_file}") export_line = f'export {var_name}="{value}"' - was_added = append_to_file(rc_file, export_line) - - if was_added: - return True - else: - return True + append_to_file(rc_file, export_line) + + try: + return export_line in rc_file.read_text(encoding="utf-8") + except OSError: + return False def set_env_var(var_name: str, value: str) -> Tuple[bool, str]: From 2c954254082ed1b17dc8bec6bb49b489a6c241cb Mon Sep 17 00:00:00 2001 From: MohamedAklamaash Date: Mon, 29 Jun 2026 15:42:29 +0530 Subject: [PATCH 4/5] Review fixes: broaden read-back catch; make key-helper failure fatal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- claude-code/gateway/setup.py | 11 +++++++---- codex/gateway/setup.py | 2 +- gemini-cli/gateway/setup.py | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/claude-code/gateway/setup.py b/claude-code/gateway/setup.py index 7d90907a..4cb56fe4 100644 --- a/claude-code/gateway/setup.py +++ b/claude-code/gateway/setup.py @@ -148,7 +148,7 @@ def set_env_var_on_unix(var_name: str, value: str) -> bool: try: return export_line in rc_file.read_text(encoding="utf-8") - except OSError: + except Exception: return False @@ -294,7 +294,7 @@ def remove_hooks_unbound_script() -> None: debug_print(f"Failed to remove {script_path}: {e}") -def setup_claude_key_helper() -> None: +def setup_claude_key_helper() -> bool: """ Create ~/.claude/anthropic_key.sh that echoes UNBOUND_API_KEY and update ~/.claude/settings.json with apiKeyHelper pointing to that script. @@ -330,8 +330,10 @@ def setup_claude_key_helper() -> None: settings["apiKeyHelper"] = "~/.claude/anthropic_key.sh" settings_path.write_text(json.dumps(settings, indent=2), encoding="utf-8") + return True except Exception as e: - print(f"⚠️ Failed to configure Claude Code key helper: {e}") + print(f"❌ Failed to configure Claude Code key helper: {e}") + return False def run_one_shot_callback_server(frontend_url: str) -> Optional[Dict[str, any]]: @@ -747,7 +749,8 @@ def main(): # Configure Claude Code helper files debug_print("Setting up Claude key helper...") - setup_claude_key_helper() + if not setup_claude_key_helper(): + return False debug_print("Claude key helper configured") # Final instructions diff --git a/codex/gateway/setup.py b/codex/gateway/setup.py index 2ddb9f3b..e4770a58 100644 --- a/codex/gateway/setup.py +++ b/codex/gateway/setup.py @@ -148,7 +148,7 @@ def set_env_var_on_unix(var_name: str, value: str) -> bool: try: return export_line in rc_file.read_text(encoding="utf-8") - except OSError: + except Exception: return False diff --git a/gemini-cli/gateway/setup.py b/gemini-cli/gateway/setup.py index 4176d5d7..9018e52b 100644 --- a/gemini-cli/gateway/setup.py +++ b/gemini-cli/gateway/setup.py @@ -148,7 +148,7 @@ def set_env_var_on_unix(var_name: str, value: str) -> bool: try: return export_line in rc_file.read_text(encoding="utf-8") - except OSError: + except Exception: return False From b2aa2248abaa6f4d025dd3f210a4ef3563febc1a Mon Sep 17 00:00:00 2001 From: MohamedAklamaash Date: Wed, 1 Jul 2026 20:42:22 +0530 Subject: [PATCH 5/5] Read-back: match active export line only, not commented occurrences MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- claude-code/gateway/setup.py | 2 +- codex/gateway/setup.py | 2 +- gemini-cli/gateway/setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/claude-code/gateway/setup.py b/claude-code/gateway/setup.py index 4cb56fe4..32ffa691 100644 --- a/claude-code/gateway/setup.py +++ b/claude-code/gateway/setup.py @@ -147,7 +147,7 @@ def set_env_var_on_unix(var_name: str, value: str) -> bool: append_to_file(rc_file, export_line) try: - return export_line in rc_file.read_text(encoding="utf-8") + return any(line.strip() == export_line for line in rc_file.read_text(encoding="utf-8").splitlines()) except Exception: return False diff --git a/codex/gateway/setup.py b/codex/gateway/setup.py index e4770a58..ee0a609e 100644 --- a/codex/gateway/setup.py +++ b/codex/gateway/setup.py @@ -147,7 +147,7 @@ def set_env_var_on_unix(var_name: str, value: str) -> bool: append_to_file(rc_file, export_line) try: - return export_line in rc_file.read_text(encoding="utf-8") + return any(line.strip() == export_line for line in rc_file.read_text(encoding="utf-8").splitlines()) except Exception: return False diff --git a/gemini-cli/gateway/setup.py b/gemini-cli/gateway/setup.py index 9018e52b..a7c4935d 100644 --- a/gemini-cli/gateway/setup.py +++ b/gemini-cli/gateway/setup.py @@ -147,7 +147,7 @@ def set_env_var_on_unix(var_name: str, value: str) -> bool: append_to_file(rc_file, export_line) try: - return export_line in rc_file.read_text(encoding="utf-8") + return any(line.strip() == export_line for line in rc_file.read_text(encoding="utf-8").splitlines()) except Exception: return False