spacehub

surshar / claude-code

Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.

Built by Anthropic · github.com/anthropics

Run @claude mentions and dedupe in auto mode on a model that supports itBrowse files
claude-sonnet-4-5 predates auto mode, so Claude Code fell back to its
default permission mode in the dedupe job. Move it and claude.yml to
claude-sonnet-4-6. The action sets --permission-mode acceptEdits for
@claude mentions and appends the workflow's claude_args after it, so the
--permission-mode auto in claude.yml wins. Drop claude.yml from
EXEMPT_FROM_AUTO_MODE.

The check now also fails when a step in auto mode names a model older than
claude-opus-4-6.
Claude authored 24 days ago parent 574ef0b · commit 7dbaea237fca
5 changed files+73 -23compared with 574ef0b
A.github/scripts/__pycache__/check_workflow_hardening.cpython-311.pyc
M.github/scripts/check_workflow_hardening.py
M.github/workflows/claude-dedupe-issues.yml
M.github/workflows/claude.yml
MCLAUDE.md
Binary file not shown.
@@ -6,6 +6,7 @@
66
77 import json
88 import pathlib
9+import re
910 import shlex
1011 import subprocess
1112 import sys
@@ -16,12 +17,18 @@
1617 SIGN_IN_MARKER = "anthropic_federation_rule_id"
1718 CLAUDE_ACTIONS = ("anthropics/claude-code-action", "anthropics/claude-code-base-action")
1819 HELP = 'See CLAUDE.md, "Security hardening for GitHub Actions".'
20+# Claude Code runs auto mode only on claude-opus-4-6 and newer models. On an older model it
21+# falls back to its default permission mode, with no safety review.
22+AUTO_MODE_MIN_VERSION = (4, 6)
23+# The version in a model name: claude-opus-4-6, claude-sonnet-4-5-20250929, claude-3-5-sonnet-latest.
24+MODEL_VERSION = re.compile(
25+ r"claude-(?:(?:opus|sonnet|haiku)-(\d+)(?:-(\d{1,2}))?"
26+ r"|(\d+)(?:-(\d{1,2}))?-(?:opus|sonnet|haiku))(?![\d.])"
27+)
1928
2029 # Key: "<workflow file name>:<job id>". Value: why that job is exempt from the table's rule.
2130 EXEMPT_FROM_FIREWALL_RUNNER: dict[str, str] = {}
22-EXEMPT_FROM_AUTO_MODE: dict[str, str] = {
23- "claude.yml:claude": "answers @claude mentions, and for those the Claude Code action sets --permission-mode acceptEdits itself",
24-}
31+EXEMPT_FROM_AUTO_MODE: dict[str, str] = {}
2532
2633
2734 def stop(message: str):
@@ -99,10 +106,12 @@
99106 return False
100107
101108
102-def permission_mode_problem(step: dict, exempt: bool) -> str | None:
109+def permission_mode_problem(step: dict, exempt: bool, inherited_env: dict) -> str | None:
103110 """The message for a step whose permission mode is wrong, or None if it is right.
104111
105- A step must set auto mode. A step of a job in EXEMPT_FROM_AUTO_MODE must set no mode at all.
112+ A step must set auto mode, on a model that supports it. A step of a job in
113+ EXEMPT_FROM_AUTO_MODE must set no mode at all. inherited_env is the workflow's and the
114+ job's 'env'.
106115 """
107116 inputs = step.get("with") or {}
108117 lines = str(inputs.get("claude_args", "")).splitlines()
@@ -141,17 +150,20 @@
141150 f"'claude_args' has '--permission-mode {mode}'. "
142151 "Change it to '--permission-mode auto'"
143152 )
153+ env = {**inherited_env, **(step.get("env") or {})}
154+ models = [
155+ ("the step's 'model'", inputs.get("model", "")),
156+ ("ANTHROPIC_MODEL", env.get("ANTHROPIC_MODEL", "")),
157+ ]
158+ models += [
159+ (f"'{flag}' in 'claude_args'", value)
160+ for flag in ("--model", "--fallback-model")
161+ for value in flag_values(args, flag)
162+ ]
144163 settings = [("the step's 'settings'", inputs.get("settings", ""))]
145164 settings += [
146- ("'--settings' in 'claude_args'", args[index + 1])
147- for index, arg in enumerate(args)
148- if arg == "--settings" and index + 1 < len(args)
165+ ("'--settings' in 'claude_args'", value) for value in flag_values(args, "--settings")
149166 ]
150- settings += [
151- ("'--settings' in 'claude_args'", arg.split("=", 1)[1])
152- for arg in args
153- if arg.startswith("--settings=")
154- ]
155167 for where, value in settings:
156168 text = settings_text(value)
157169 if text is None:
@@ -161,9 +173,42 @@
161173 )
162174 if "defaultMode" in text:
163175 return f"remove 'defaultMode' from {where}: settings must not set a permission mode"
176+ try:
177+ parsed = json.loads(text) if text else {}
178+ except ValueError:
179+ parsed = {}
180+ if isinstance(parsed, dict) and "model" in parsed:
181+ models.append((f"'model' in {where}", parsed["model"]))
182+ if not exempt:
183+ for where, model in models:
184+ if predates_auto_mode(str(model or "")):
185+ return (
186+ f"{where} is '{model}', which Claude Code does not run in auto mode: it "
187+ "falls back to the default permission mode. Use claude-opus-4-6 or a newer model"
188+ )
164189 return None
165190
166191
192+def flag_values(args: list[str], flag: str) -> list[str]:
193+ """The values a flag in claude_args is given, as '--flag value' or '--flag=value'."""
194+ values = [
195+ args[index + 1] for index, arg in enumerate(args) if arg == flag and index + 1 < len(args)
196+ ]
197+ return values + [arg.split("=", 1)[1] for arg in args if arg.startswith(f"{flag}=")]
198+
199+
200+def predates_auto_mode(model: str) -> bool:
201+ """Whether the model is older than AUTO_MODE_MIN_VERSION. A name with no version, such as
202+ 'opus' or 'default', stands for a current model, except 'haiku' (claude-haiku-4-5)."""
203+ if model.strip().lower() == "haiku":
204+ return True
205+ match = MODEL_VERSION.search(model.lower())
206+ if not match:
207+ return False
208+ major, minor = match.group(1, 2) if match.group(1) else match.group(3, 4)
209+ return (int(major), int(minor or 0)) < AUTO_MODE_MIN_VERSION
210+
211+
167212 def settings_text(value) -> str | None:
168213 """The settings JSON a 'settings' value stands for: the value itself, or the contents of
169214 the file it names when it is a path inside the repository. None when it names a path
@@ -183,7 +228,7 @@
183228 return text
184229
185230
186-def check_job(file_name: str, job_id: str, job: dict) -> list[str]:
231+def check_job(file_name: str, job_id: str, job: dict, workflow_env: dict) -> list[str]:
187232 key = f"{file_name}:{job_id}"
188233 where = f".github/workflows/{file_name}: job '{job_id}'"
189234 errors = []
@@ -221,7 +266,8 @@
221266 for index, step in enumerate(steps_of(job), start=1):
222267 if not runs_claude_code_action(step):
223268 continue
224- problem = permission_mode_problem(step, exempt)
269+ inherited_env = {**workflow_env, **(job.get("env") or {})}
270+ problem = permission_mode_problem(step, exempt, inherited_env)
225271 if problem:
226272 step_label = f"step '{step['name']}'" if "name" in step else f"step {index}"
227273 errors.append(f"{where}, {step_label}: {problem}. {HELP}")
@@ -275,7 +321,7 @@
275321 if not isinstance(job, dict) or not job_calls_claude(job):
276322 continue
277323 checked += 1
278- errors.extend(check_job(path.name, job_id, job))
324+ errors.extend(check_job(path.name, job_id, job, workflow.get("env") or {}))
279325 if checked:
280326 errors.extend(check_policy())
281327 for error in errors:
@@ -45,7 +45,7 @@
4545 # --permission-mode auto: a tool call that needs permission and is outside the allowed-tools
4646 # list in .claude/commands/dedupe.md runs only if Claude Code's safety review passes it.
4747 # Tools in --disallowedTools never run.
48- claude_args: '--model claude-sonnet-4-5-20250929 --permission-mode auto --disallowedTools "WebFetch,WebSearch,Write,Edit,MultiEdit,NotebookEdit"'
48+ claude_args: '--model claude-sonnet-4-6 --permission-mode auto --disallowedTools "WebFetch,WebSearch,Write,Edit,MultiEdit,NotebookEdit"'
4949
5050 - name: Log duplicate comment event to Statsig
5151 if: always()
@@ -42,5 +42,8 @@
4242 anthropic_organization_id: ${{ vars.ANTHROPIC_ORGANIZATION_ID }}
4343 anthropic_service_account_id: ${{ vars.ANTHROPIC_SERVICE_ACCOUNT_ID }}
4444 anthropic_workspace_id: ${{ vars.ANTHROPIC_WORKSPACE_ID }}
45- claude_args: "--model claude-sonnet-4-5-20250929"
45+ # --permission-mode auto: a tool call that needs permission and that the allowed tools
46+ # do not cover runs only if Claude Code's safety review passes it. The action sets
47+ # --permission-mode acceptEdits for @claude mentions, and this one, which comes after it, wins.
48+ claude_args: "--model claude-sonnet-4-6 --permission-mode auto"
4649
MCLAUDE.md+6 -5
@@ -19,12 +19,13 @@
1919 `claude_args` input) passes `--permission-mode auto` in `claude_args`. A tool
2020 call that needs permission and that the allowed tools do not cover then runs
2121 only if Claude Code's safety review passes it. Allow only the tools the job
22- needs, and keep any `--disallowedTools` list a step has.
22+ needs, and keep any `--disallowedTools` list a step has. Use `claude-opus-4-6`
23+ or a newer model: on an older one Claude Code falls back to its default
24+ permission mode.
2325
24-Exception (listed with its reason in an exemption table in
25-`.github/scripts/check_workflow_hardening.py`): `claude.yml` answers `@claude`
26-mentions. For those the Claude Code action sets `--permission-mode acceptEdits`
27-itself. Do not add a `--permission-mode` there.
26+`claude.yml` answers `@claude` mentions. The Claude Code action sets
27+`--permission-mode acceptEdits` for those, and the `--permission-mode auto` in
28+the workflow's `claude_args`, which comes after it, replaces it.
2829
2930 `.github/workflows/workflow-hardening.yml` fails when a job that runs the Claude
3031 Code action or mentions `ANTHROPIC_FEDERATION_RULE_ID` breaks protection 1 or 3,