code-mower 0.5.0b5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- code_mower/__init__.py +3 -0
- code_mower/adapters/__init__.py +39 -0
- code_mower/adapters/_base.py +148 -0
- code_mower/adapters/cursor_bugbot.py +97 -0
- code_mower/adapters/gitar.py +111 -0
- code_mower/adapters/greptile.py +191 -0
- code_mower/adapters/qodo.py +140 -0
- code_mower/antigravity_cli_audit_pr.py +241 -0
- code_mower/audit_handoff_log.py +345 -0
- code_mower/audit_labeler_lib.py +311 -0
- code_mower/audit_progress.py +203 -0
- code_mower/blind_review_artifacts.py +562 -0
- code_mower/blind_review_coordinator.py +276 -0
- code_mower/bootstrap.py +524 -0
- code_mower/builder_experiment.py +539 -0
- code_mower/calibration/__init__.py +181 -0
- code_mower/calibration/arms.py +267 -0
- code_mower/calibration/auto_discovery.py +348 -0
- code_mower/calibration/commands.py +192 -0
- code_mower/calibration/context_inputs.py +199 -0
- code_mower/calibration/corpus.py +91 -0
- code_mower/calibration/evidence.py +20 -0
- code_mower/calibration/evidence_report.py +360 -0
- code_mower/calibration/identity.py +27 -0
- code_mower/calibration/metrics.py +18 -0
- code_mower/calibration/overlap.py +103 -0
- code_mower/calibration/planning.py +309 -0
- code_mower/calibration/policy.py +207 -0
- code_mower/calibration/results.py +315 -0
- code_mower/calibration/run_results.py +147 -0
- code_mower/calibration/run_status.py +64 -0
- code_mower/calibration/runner.py +360 -0
- code_mower/calibration/truth.py +188 -0
- code_mower/calibration/value_report.py +142 -0
- code_mower/checks.py +402 -0
- code_mower/claude_audit_pr.py +1126 -0
- code_mower/claude_cli_bounce.py +303 -0
- code_mower/claude_cli_environment.py +73 -0
- code_mower/clear_stale.py +374 -0
- code_mower/cli.py +537 -0
- code_mower/cloud.py +674 -0
- code_mower/cloud_client/__init__.py +173 -0
- code_mower/cloud_client/bundle.py +155 -0
- code_mower/cloud_client/doctor.py +206 -0
- code_mower/cloud_client/dogfood.py +78 -0
- code_mower/cloud_client/endpoints.py +114 -0
- code_mower/cloud_client/errors.py +7 -0
- code_mower/cloud_client/events.py +278 -0
- code_mower/cloud_client/export.py +272 -0
- code_mower/cloud_client/git_metadata.py +46 -0
- code_mower/cloud_client/manifest.py +39 -0
- code_mower/cloud_client/operations.py +448 -0
- code_mower/cloud_client/reports.py +45 -0
- code_mower/cloud_client/setup.py +205 -0
- code_mower/cloud_client/upload.py +97 -0
- code_mower/code_mower_calibration.py +598 -0
- code_mower/code_mower_context_packs.py +591 -0
- code_mower/code_mower_merge.py +227 -0
- code_mower/code_mower_telemetry.py +561 -0
- code_mower/coderabbit_cli_audit_pr.py +526 -0
- code_mower/codex_audit_env_preflight.py +220 -0
- code_mower/codex_audit_pr.py +1738 -0
- code_mower/codex_audit_schema_smoke.py +160 -0
- code_mower/codex_audit_verdict.schema.json +44 -0
- code_mower/config.py +655 -0
- code_mower/doctor.py +161 -0
- code_mower/doctor_checks/__init__.py +104 -0
- code_mower/doctor_checks/cloud.py +129 -0
- code_mower/doctor_checks/common.py +215 -0
- code_mower/doctor_checks/github.py +128 -0
- code_mower/doctor_checks/github_actions.py +11 -0
- code_mower/doctor_checks/github_actions_cost.py +99 -0
- code_mower/doctor_checks/github_actions_cost_summary.py +111 -0
- code_mower/doctor_checks/github_actions_failure_annotations.py +27 -0
- code_mower/doctor_checks/github_actions_failure_models.py +47 -0
- code_mower/doctor_checks/github_actions_failure_scan.py +200 -0
- code_mower/doctor_checks/github_actions_failure_selection.py +63 -0
- code_mower/doctor_checks/github_actions_failures.py +103 -0
- code_mower/doctor_checks/github_actions_permissions.py +55 -0
- code_mower/doctor_checks/github_api.py +79 -0
- code_mower/doctor_checks/github_branch.py +56 -0
- code_mower/doctor_checks/github_config.py +25 -0
- code_mower/doctor_checks/github_provider.py +61 -0
- code_mower/doctor_checks/github_repo.py +120 -0
- code_mower/doctor_checks/groups.py +36 -0
- code_mower/doctor_checks/models.py +96 -0
- code_mower/doctor_checks/output.py +86 -0
- code_mower/doctor_checks/presets.py +64 -0
- code_mower/doctor_checks/privacy.py +20 -0
- code_mower/doctor_checks/provider_api_model.py +138 -0
- code_mower/doctor_checks/provider_api_model_openai.py +29 -0
- code_mower/doctor_checks/provider_api_model_profiles.py +137 -0
- code_mower/doctor_checks/provider_env.py +113 -0
- code_mower/doctor_checks/provider_env_required.py +56 -0
- code_mower/doctor_checks/provider_env_tokens.py +100 -0
- code_mower/doctor_checks/provider_local_cli.py +162 -0
- code_mower/doctor_checks/provider_local_cli_commands.py +47 -0
- code_mower/doctor_checks/provider_local_cli_probe_config.py +70 -0
- code_mower/doctor_checks/provider_probe.py +20 -0
- code_mower/doctor_checks/provider_probe_auth.py +52 -0
- code_mower/doctor_checks/provider_probe_evaluation.py +109 -0
- code_mower/doctor_checks/provider_probe_json.py +45 -0
- code_mower/doctor_checks/provider_probe_remediation.py +39 -0
- code_mower/doctor_checks/providers.py +159 -0
- code_mower/doctor_checks/registry.py +69 -0
- code_mower/doctor_checks/runner.py +188 -0
- code_mower/doctor_checks/runtime.py +89 -0
- code_mower/doctor_checks/runtime_github_auth.py +148 -0
- code_mower/gemini_cli_audit_pr.py +897 -0
- code_mower/hermes_cli_audit_pr.py +436 -0
- code_mower/init.py +888 -0
- code_mower/lane_configs/__init__.py +37 -0
- code_mower/lane_configs/aider.py +32 -0
- code_mower/lane_configs/antigravity_cli.py +35 -0
- code_mower/lane_configs/claude.py +35 -0
- code_mower/lane_configs/codex.py +32 -0
- code_mower/lane_configs/devin.py +33 -0
- code_mower/lane_configs/gemini_cli.py +35 -0
- code_mower/lane_configs/hermes_cli.py +35 -0
- code_mower/lane_configs/local_llm.py +31 -0
- code_mower/local_llm_audit_pr.py +1364 -0
- code_mower/local_llm_bakeoff.py +458 -0
- code_mower/local_llm_calibration.py +441 -0
- code_mower/local_llm_profiles.py +66 -0
- code_mower/migration.py +508 -0
- code_mower/migration_install.py +292 -0
- code_mower/migration_mirror.py +392 -0
- code_mower/migration_readiness.py +237 -0
- code_mower/migration_rehearsal.py +718 -0
- code_mower/next_steps.py +441 -0
- code_mower/package.py +673 -0
- code_mower/package_content.py +444 -0
- code_mower/package_manifest.py +452 -0
- code_mower/package_paths.py +53 -0
- code_mower/package_rendering.py +90 -0
- code_mower/package_static.py +585 -0
- code_mower/prompts.py +267 -0
- code_mower/provider_registry.py +469 -0
- code_mower/provider_runners/__init__.py +60 -0
- code_mower/provider_runners/comments.py +31 -0
- code_mower/provider_runners/git.py +46 -0
- code_mower/provider_runners/github_auth.py +61 -0
- code_mower/provider_runners/github_pr.py +120 -0
- code_mower/provider_runners/process.py +58 -0
- code_mower/provider_runners/repo_paths.py +23 -0
- code_mower/provider_runners/text_schema.py +41 -0
- code_mower/provider_runners/verdict_artifacts.py +103 -0
- code_mower/provider_runners/workspace.py +57 -0
- code_mower/release_readiness.py +549 -0
- code_mower/reviewer_metrics.py +389 -0
- code_mower/saas_reviewer_labeler.py +809 -0
- code_mower/secrets.py +89 -0
- code_mower/templates/builder-experiment.example.json +55 -0
- code_mower/templates/calibration-corpus.example.json +129 -0
- code_mower/templates/calibration-corpus.json +129 -0
- code_mower/templates/code-mower.example.yml +423 -0
- code_mower/templates/context-packs.example.json +150 -0
- code_mower/templates/lane_prompts/base-audit.md +22 -0
- code_mower/templates/lane_prompts/calibration-policy.md +21 -0
- code_mower/templates/lane_prompts/context-driven-quality.md +21 -0
- code_mower/templates/lane_prompts/docs-design.md +12 -0
- code_mower/templates/lane_prompts/generic-programming.md +21 -0
- code_mower/templates/lane_prompts/operability.md +22 -0
- code_mower/templates/lane_prompts/package-runtime.md +12 -0
- code_mower/templates/lane_prompts/security-threat-model.md +22 -0
- code_mower/templates/product-support/code_mower +216 -0
- code_mower/templates/product-support/code_mower_standalone_pin.env +7 -0
- code_mower/templates/product-support/code_mower_standalone_shadow.sh +151 -0
- code_mower/templates/product-support/run_claude_audit_pr.sh +32 -0
- code_mower/templates/product-support/run_codex_audit_pr.sh +32 -0
- code_mower/templates/product-support/safe_gh_comment.py +96 -0
- code_mower/templates/providers.yml +454 -0
- code_mower/templates/reviewer-spend.example.json +28 -0
- code_mower/templates/reviewer-value-report.example.md +20 -0
- code_mower/templates/workflows/private-standalone-shadow.yml.j2 +106 -0
- code_mower/templates/workflows/review-clear-stale.yml.j2 +83 -0
- code_mower/trailer_comment_labeler.py +207 -0
- code_mower/versioning.py +32 -0
- code_mower-0.5.0b5.dist-info/METADATA +302 -0
- code_mower-0.5.0b5.dist-info/RECORD +185 -0
- code_mower-0.5.0b5.dist-info/WHEEL +5 -0
- code_mower-0.5.0b5.dist-info/entry_points.txt +2 -0
- code_mower-0.5.0b5.dist-info/licenses/LICENSE +202 -0
- code_mower-0.5.0b5.dist-info/licenses/NOTICE +10 -0
- code_mower-0.5.0b5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Diagnose and bounce Claude CLI auth/env issues."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
import tempfile
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Mapping, Sequence
|
|
14
|
+
|
|
15
|
+
if __package__ in {None, "", "tools"}:
|
|
16
|
+
try:
|
|
17
|
+
from tools.claude_cli_environment import (
|
|
18
|
+
CLAUDE_AUTH_OVERRIDE_ENV,
|
|
19
|
+
clean_claude_cli_env,
|
|
20
|
+
render_claude_env_unset_snippet,
|
|
21
|
+
)
|
|
22
|
+
from tools.doctor_checks.provider_probe import evaluate_json_probe
|
|
23
|
+
except ImportError: # pragma: no cover - direct script execution fallback
|
|
24
|
+
from claude_cli_environment import ( # type: ignore
|
|
25
|
+
CLAUDE_AUTH_OVERRIDE_ENV,
|
|
26
|
+
clean_claude_cli_env,
|
|
27
|
+
render_claude_env_unset_snippet,
|
|
28
|
+
)
|
|
29
|
+
from doctor_checks.provider_probe import evaluate_json_probe # type: ignore
|
|
30
|
+
else: # pragma: no cover - exercised after package extraction.
|
|
31
|
+
from .claude_cli_environment import (
|
|
32
|
+
CLAUDE_AUTH_OVERRIDE_ENV,
|
|
33
|
+
clean_claude_cli_env,
|
|
34
|
+
render_claude_env_unset_snippet,
|
|
35
|
+
)
|
|
36
|
+
from .doctor_checks.provider_probe import evaluate_json_probe
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
DEFAULT_PROBE_ARGS = (
|
|
40
|
+
"--print",
|
|
41
|
+
"--output-format",
|
|
42
|
+
"json",
|
|
43
|
+
"--no-session-persistence",
|
|
44
|
+
"--setting-sources",
|
|
45
|
+
"local",
|
|
46
|
+
"--strict-mcp-config",
|
|
47
|
+
"--mcp-config",
|
|
48
|
+
'{"mcpServers":{}}',
|
|
49
|
+
"--disable-slash-commands",
|
|
50
|
+
"--tools",
|
|
51
|
+
"",
|
|
52
|
+
"--model",
|
|
53
|
+
"sonnet",
|
|
54
|
+
"--max-budget-usd",
|
|
55
|
+
"0.25",
|
|
56
|
+
"Reply with exactly: ok",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
PROBE_CONFIG = {
|
|
60
|
+
"doctor_probe_error_fields": ("is_error", "api_error_status"),
|
|
61
|
+
"doctor_probe_auth_status_fields": ("api_error_status",),
|
|
62
|
+
"doctor_probe_expect_json_field": "result",
|
|
63
|
+
"doctor_probe_expect_json_value": "ok",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _run_probe(
|
|
68
|
+
*,
|
|
69
|
+
command_path: str,
|
|
70
|
+
args: Sequence[str],
|
|
71
|
+
timeout_seconds: int,
|
|
72
|
+
env: Mapping[str, str],
|
|
73
|
+
) -> tuple[subprocess.CompletedProcess[str] | None, str | None]:
|
|
74
|
+
with tempfile.TemporaryDirectory(prefix="code-mower-claude-bounce-") as tmp:
|
|
75
|
+
try:
|
|
76
|
+
return (
|
|
77
|
+
subprocess.run(
|
|
78
|
+
[command_path, *args],
|
|
79
|
+
cwd=tmp,
|
|
80
|
+
env=dict(env),
|
|
81
|
+
capture_output=True,
|
|
82
|
+
text=True,
|
|
83
|
+
check=False,
|
|
84
|
+
timeout=timeout_seconds,
|
|
85
|
+
),
|
|
86
|
+
None,
|
|
87
|
+
)
|
|
88
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
89
|
+
return None, str(exc)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _summarize_probe(
|
|
93
|
+
*,
|
|
94
|
+
label: str,
|
|
95
|
+
completed: subprocess.CompletedProcess[str] | None,
|
|
96
|
+
exception: str | None,
|
|
97
|
+
removed_env: tuple[str, ...],
|
|
98
|
+
) -> dict[str, Any]:
|
|
99
|
+
if completed is None:
|
|
100
|
+
return {
|
|
101
|
+
"label": label,
|
|
102
|
+
"status": "warn",
|
|
103
|
+
"message": "probe could not run",
|
|
104
|
+
"returncode": None,
|
|
105
|
+
"removed_env": list(removed_env),
|
|
106
|
+
"exception_type": "probe_exception",
|
|
107
|
+
}
|
|
108
|
+
output = (completed.stdout or completed.stderr or "").strip()
|
|
109
|
+
status, message, detail = evaluate_json_probe(
|
|
110
|
+
PROBE_CONFIG,
|
|
111
|
+
output,
|
|
112
|
+
returncode=completed.returncode,
|
|
113
|
+
)
|
|
114
|
+
return {
|
|
115
|
+
"label": label,
|
|
116
|
+
"status": status,
|
|
117
|
+
"message": message,
|
|
118
|
+
"returncode": completed.returncode,
|
|
119
|
+
"removed_env": list(removed_env),
|
|
120
|
+
**detail,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def bounce_claude_cli(
|
|
125
|
+
*,
|
|
126
|
+
command: str = "claude",
|
|
127
|
+
probe_args: tuple[str, ...] = DEFAULT_PROBE_ARGS,
|
|
128
|
+
timeout_seconds: int = 30,
|
|
129
|
+
base_env: Mapping[str, str] | None = None,
|
|
130
|
+
extra_unset: tuple[str, ...] = (),
|
|
131
|
+
skip_inherited_probe: bool = False,
|
|
132
|
+
) -> dict[str, Any]:
|
|
133
|
+
env = dict(os.environ if base_env is None else base_env)
|
|
134
|
+
command_path = shutil.which(command, path=env.get("PATH")) or ""
|
|
135
|
+
if not command_path:
|
|
136
|
+
return {
|
|
137
|
+
"status": "fail",
|
|
138
|
+
"command": command,
|
|
139
|
+
"command_path": None,
|
|
140
|
+
"probes": [],
|
|
141
|
+
"recommendation": "install_or_fix_claude_path",
|
|
142
|
+
"message": f"{command} was not found on PATH",
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
probes: list[dict[str, Any]] = []
|
|
146
|
+
inherited_summary: dict[str, Any] | None = None
|
|
147
|
+
if not skip_inherited_probe:
|
|
148
|
+
completed, exception = _run_probe(
|
|
149
|
+
command_path=command_path,
|
|
150
|
+
args=probe_args,
|
|
151
|
+
timeout_seconds=timeout_seconds,
|
|
152
|
+
env=env,
|
|
153
|
+
)
|
|
154
|
+
inherited_summary = _summarize_probe(
|
|
155
|
+
label="inherited_env",
|
|
156
|
+
completed=completed,
|
|
157
|
+
exception=exception,
|
|
158
|
+
removed_env=(),
|
|
159
|
+
)
|
|
160
|
+
probes.append(inherited_summary)
|
|
161
|
+
|
|
162
|
+
clean_env, removed = clean_claude_cli_env(
|
|
163
|
+
env,
|
|
164
|
+
scrub_auth_overrides=True,
|
|
165
|
+
extra_unset=extra_unset,
|
|
166
|
+
)
|
|
167
|
+
completed, exception = _run_probe(
|
|
168
|
+
command_path=command_path,
|
|
169
|
+
args=probe_args,
|
|
170
|
+
timeout_seconds=timeout_seconds,
|
|
171
|
+
env=clean_env,
|
|
172
|
+
)
|
|
173
|
+
clean_summary = _summarize_probe(
|
|
174
|
+
label="clean_env",
|
|
175
|
+
completed=completed,
|
|
176
|
+
exception=exception,
|
|
177
|
+
removed_env=removed,
|
|
178
|
+
)
|
|
179
|
+
probes.append(clean_summary)
|
|
180
|
+
|
|
181
|
+
inherited_ok = bool(inherited_summary and inherited_summary.get("status") == "pass")
|
|
182
|
+
clean_ok = clean_summary.get("status") == "pass"
|
|
183
|
+
clean_auth_error = bool(clean_summary.get("auth_error_detected"))
|
|
184
|
+
inherited_auth_error = bool(
|
|
185
|
+
inherited_summary and inherited_summary.get("auth_error_detected")
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
if inherited_ok:
|
|
189
|
+
status = "pass"
|
|
190
|
+
recommendation = "no_action_needed"
|
|
191
|
+
message = "Claude CLI smoke probe passed with the inherited environment"
|
|
192
|
+
elif clean_ok:
|
|
193
|
+
status = "pass"
|
|
194
|
+
recommendation = "use_clean_claude_env_or_restart_parent_app"
|
|
195
|
+
message = (
|
|
196
|
+
"Claude CLI passed after removing Claude/Anthropic auth override env vars; "
|
|
197
|
+
"source the generated unset snippet or restart the parent app with a clean environment"
|
|
198
|
+
)
|
|
199
|
+
elif clean_auth_error:
|
|
200
|
+
status = "warn"
|
|
201
|
+
recommendation = "reauthenticate_claude"
|
|
202
|
+
message = "Claude CLI still reports auth failure after env cleanup"
|
|
203
|
+
elif inherited_auth_error:
|
|
204
|
+
status = "warn"
|
|
205
|
+
recommendation = "clean_env_did_not_resolve_auth"
|
|
206
|
+
message = "Inherited env had auth failure, and the clean probe did not pass"
|
|
207
|
+
else:
|
|
208
|
+
status = "warn"
|
|
209
|
+
recommendation = "inspect_cli_output_manually"
|
|
210
|
+
message = "Claude CLI probe did not pass; inspect local CLI install/auth manually"
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
"status": status,
|
|
214
|
+
"command": command,
|
|
215
|
+
"command_path": command_path,
|
|
216
|
+
"probe_args": list(probe_args),
|
|
217
|
+
"scrubbed_env_names": list(CLAUDE_AUTH_OVERRIDE_ENV),
|
|
218
|
+
"extra_unset": list(extra_unset),
|
|
219
|
+
"probes": probes,
|
|
220
|
+
"recommendation": recommendation,
|
|
221
|
+
"message": message,
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def render_bounce_text(report: Mapping[str, Any]) -> str:
|
|
226
|
+
lines = [
|
|
227
|
+
"Code Mower Claude CLI bounce",
|
|
228
|
+
f"Status: {report.get('status', 'unknown')}",
|
|
229
|
+
f"Command: {report.get('command')} ({report.get('command_path') or 'not found'})",
|
|
230
|
+
f"Recommendation: {report.get('recommendation')}",
|
|
231
|
+
f"Message: {report.get('message')}",
|
|
232
|
+
"",
|
|
233
|
+
]
|
|
234
|
+
for probe in report.get("probes", []):
|
|
235
|
+
if not isinstance(probe, Mapping):
|
|
236
|
+
continue
|
|
237
|
+
removed = ", ".join(probe.get("removed_env", [])) or "none"
|
|
238
|
+
lines.append(
|
|
239
|
+
f"- {probe.get('label')}: {probe.get('status')} "
|
|
240
|
+
f"returncode={probe.get('returncode')} removed_env={removed}"
|
|
241
|
+
)
|
|
242
|
+
message = probe.get("message")
|
|
243
|
+
if message:
|
|
244
|
+
lines.append(f" {message}")
|
|
245
|
+
auth_status = probe.get("auth_status_code")
|
|
246
|
+
if auth_status:
|
|
247
|
+
lines.append(f" auth_status_code={auth_status}")
|
|
248
|
+
lines.extend(
|
|
249
|
+
[
|
|
250
|
+
"",
|
|
251
|
+
"If clean_env passes but inherited_env fails, source the generated unset",
|
|
252
|
+
"snippet in your shell or fully restart the app that launched the shell.",
|
|
253
|
+
]
|
|
254
|
+
)
|
|
255
|
+
return "\n".join(lines) + "\n"
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
259
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
260
|
+
parser.add_argument("--command", default=os.environ.get("CLAUDE_CLI_PATH", "claude"))
|
|
261
|
+
parser.add_argument("--timeout", type=int, default=30)
|
|
262
|
+
parser.add_argument(
|
|
263
|
+
"--skip-inherited-probe",
|
|
264
|
+
action="store_true",
|
|
265
|
+
help="only run the clean-env Claude probe",
|
|
266
|
+
)
|
|
267
|
+
parser.add_argument(
|
|
268
|
+
"--unset-env",
|
|
269
|
+
action="append",
|
|
270
|
+
default=[],
|
|
271
|
+
help="additional env var to remove for the clean probe; repeatable",
|
|
272
|
+
)
|
|
273
|
+
parser.add_argument(
|
|
274
|
+
"--write-env",
|
|
275
|
+
type=Path,
|
|
276
|
+
default=None,
|
|
277
|
+
help="write a shell snippet that unsets Claude/Anthropic auth override vars",
|
|
278
|
+
)
|
|
279
|
+
parser.add_argument("--json", action="store_true")
|
|
280
|
+
args = parser.parse_args(argv)
|
|
281
|
+
|
|
282
|
+
report = bounce_claude_cli(
|
|
283
|
+
command=args.command,
|
|
284
|
+
timeout_seconds=args.timeout,
|
|
285
|
+
extra_unset=tuple(args.unset_env),
|
|
286
|
+
skip_inherited_probe=args.skip_inherited_probe,
|
|
287
|
+
)
|
|
288
|
+
if args.write_env is not None:
|
|
289
|
+
args.write_env.parent.mkdir(parents=True, exist_ok=True)
|
|
290
|
+
args.write_env.write_text(render_claude_env_unset_snippet(), encoding="utf-8")
|
|
291
|
+
report["written_env_file"] = str(args.write_env)
|
|
292
|
+
|
|
293
|
+
if args.json:
|
|
294
|
+
print(json.dumps(report, indent=2, sort_keys=True))
|
|
295
|
+
else:
|
|
296
|
+
print(render_bounce_text(report), end="")
|
|
297
|
+
if args.write_env is not None:
|
|
298
|
+
print(f"Wrote env cleanup snippet: {args.write_env}")
|
|
299
|
+
return 0 if report.get("status") == "pass" else 1
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
if __name__ == "__main__":
|
|
303
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Claude CLI environment cleanup helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
CLAUDE_AUTH_OVERRIDE_ENV = (
|
|
9
|
+
"ANTHROPIC_API_KEY",
|
|
10
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
11
|
+
"CLAUDE_API_KEY",
|
|
12
|
+
"CLAUDE_CONFIG_DIR",
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
GITHUB_TOKEN_ENV = ("GITHUB_TOKEN", "GH_TOKEN")
|
|
16
|
+
|
|
17
|
+
_TRUTHY = {"1", "true", "yes", "on"}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def env_flag(value: str | None) -> bool:
|
|
21
|
+
return (value or "").strip().lower() in _TRUTHY
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def clean_claude_cli_env(
|
|
25
|
+
base_env: Mapping[str, str],
|
|
26
|
+
*,
|
|
27
|
+
unset_github_tokens: bool = False,
|
|
28
|
+
scrub_auth_overrides: bool = True,
|
|
29
|
+
extra_unset: tuple[str, ...] = (),
|
|
30
|
+
) -> tuple[dict[str, str], tuple[str, ...]]:
|
|
31
|
+
"""Return a Claude CLI child environment with known stale overrides removed."""
|
|
32
|
+
|
|
33
|
+
env = dict(base_env)
|
|
34
|
+
names: list[str] = []
|
|
35
|
+
if unset_github_tokens:
|
|
36
|
+
names.extend(GITHUB_TOKEN_ENV)
|
|
37
|
+
if scrub_auth_overrides:
|
|
38
|
+
names.extend(CLAUDE_AUTH_OVERRIDE_ENV)
|
|
39
|
+
names.extend(extra_unset)
|
|
40
|
+
|
|
41
|
+
removed: list[str] = []
|
|
42
|
+
for name in dict.fromkeys(name for name in names if name):
|
|
43
|
+
if name in env:
|
|
44
|
+
removed.append(name)
|
|
45
|
+
env.pop(name, None)
|
|
46
|
+
return env, tuple(removed)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def render_claude_env_unset_snippet(
|
|
50
|
+
*,
|
|
51
|
+
include_comments: bool = True,
|
|
52
|
+
names: tuple[str, ...] = CLAUDE_AUTH_OVERRIDE_ENV,
|
|
53
|
+
) -> str:
|
|
54
|
+
lines: list[str] = []
|
|
55
|
+
if include_comments:
|
|
56
|
+
lines.extend(
|
|
57
|
+
[
|
|
58
|
+
"# Source this file to remove Claude/Anthropic auth overrides",
|
|
59
|
+
"# from the current shell before running Claude Code.",
|
|
60
|
+
"# It does not delete Claude credentials or modify your keychain.",
|
|
61
|
+
]
|
|
62
|
+
)
|
|
63
|
+
lines.extend(f"unset {name}" for name in names)
|
|
64
|
+
return "\n".join(lines) + "\n"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
__all__ = (
|
|
68
|
+
"CLAUDE_AUTH_OVERRIDE_ENV",
|
|
69
|
+
"GITHUB_TOKEN_ENV",
|
|
70
|
+
"clean_claude_cli_env",
|
|
71
|
+
"env_flag",
|
|
72
|
+
"render_claude_env_unset_snippet",
|
|
73
|
+
)
|