Skip to content
Open
42 changes: 42 additions & 0 deletions src/specify_cli/integrations/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@

from __future__ import annotations

import os
import re
import shlex
import shutil
from abc import ABC
from dataclasses import dataclass
Expand Down Expand Up @@ -138,6 +140,43 @@ def build_exec_args(
"""
return None

def _apply_extra_args_env_var(self, args: list[str]) -> None:
"""Append `SPECIFY_INTEGRATION_<KEY>_EXTRA_ARGS` env-var value to *args*.

Operators can inject extra CLI flags into the spawned agent
subprocess by setting an env var named for the integration key,
e.g. `SPECIFY_INTEGRATION_CLAUDE_EXTRA_ARGS="--dangerously-skip-permissions"`.
The `INTEGRATION` segment scopes the variable to this subsystem
so it does not collide with other Spec Kit env-var namespaces.
Hyphens in the integration key are replaced with underscores
and the key is uppercased
(e.g. `kiro-cli` → `SPECIFY_INTEGRATION_KIRO_CLI_EXTRA_ARGS`).

Useful in CI / non-interactive contexts where the spawned agent
needs flags that change its prompt-handling behaviour.
Default behaviour (env var unset or whitespace-only) is a no-op
— *args* is unchanged. Multi-token values are parsed via
`shlex.split`.

See issue #2595.
"""
env_name = (
f"SPECIFY_INTEGRATION_{self.key.upper().replace('-', '_')}_EXTRA_ARGS"
)
extra = os.environ.get(env_name, "").strip()
if not extra:
return
try:
tokens = shlex.split(extra)
except ValueError as exc:
raise ValueError(
f"{env_name} is not parseable as a POSIX-quoted command line "
f"(value: {extra!r}). shlex reported: {exc}. "
f"Use single or double quotes to group multi-word values, e.g. "
f'{env_name}=\'--flag "value with spaces"\'.'
) from exc
args.extend(tokens)

def build_command_invocation(self, command_name: str, args: str = "") -> str:
"""Build the native slash-command invocation for a Spec Kit command.

Expand Down Expand Up @@ -851,6 +890,7 @@ def build_exec_args(
if not self.config or not self.config.get("requires_cli"):
return None
args = [self.key, "-p", prompt]
self._apply_extra_args_env_var(args)
if model:
args.extend(["--model", model])
if output_json:
Expand Down Expand Up @@ -938,6 +978,7 @@ def build_exec_args(
if not self.config or not self.config.get("requires_cli"):
return None
args = [self.key, "-p", prompt]
self._apply_extra_args_env_var(args)
if model:
args.extend(["-m", model])
if output_json:
Expand Down Expand Up @@ -1356,6 +1397,7 @@ def build_exec_args(
if not self.config or not self.config.get("requires_cli"):
return None
args = [self.key, "-p", prompt]
self._apply_extra_args_env_var(args)
if model:
args.extend(["--model", model])
if output_json:
Expand Down
7 changes: 6 additions & 1 deletion src/specify_cli/integrations/codex/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,12 @@ def build_exec_args(
output_json: bool = True,
) -> list[str] | None:
# Codex uses ``codex exec "prompt"`` for non-interactive mode.
args: list[str] = ["codex", "exec", prompt]
# Use ``self.key`` so the executable name stays coupled to the
# env-var lookup (which also derives from ``self.key``), matching
# the pattern in Devin/Opencode and avoiding drift if the key
# ever changes.
args: list[str] = [self.key, "exec", prompt]
self._apply_extra_args_env_var(args)
if model:
args.extend(["--model", model])
if output_json:
Expand Down
12 changes: 11 additions & 1 deletion src/specify_cli/integrations/copilot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ def build_exec_args(
# (default: enabled). The deprecated SPECKIT_ALLOW_ALL_TOOLS
# is also honoured as a fallback.
args = [_copilot_executable(), "-p", prompt]
self._apply_extra_args_env_var(args)
if _allow_all():
args.append("--yolo")
if model:
Expand Down Expand Up @@ -217,6 +218,11 @@ def dispatch_command(
prompt = args or ""

cli_args = [_copilot_executable(), "-p", prompt]
# Honour SPECIFY_INTEGRATION_COPILOT_EXTRA_ARGS for real workflow
# runs. `dispatch_command` builds cli_args inline rather than
# going through `build_exec_args`, so the hook must be invoked
# here too — otherwise the env var is silently ignored.
self._apply_extra_args_env_var(cli_args)
if not skills_mode:
cli_args.extend(["--agent", agent_name])
if _allow_all():
Expand Down Expand Up @@ -368,7 +374,11 @@ def _setup_default(
created: list[Path] = []

script_type = opts.get("script_type", "sh")
arg_placeholder = self.registrar_config.get("args", "$ARGUMENTS")
arg_placeholder = (
self.registrar_config.get("args", "$ARGUMENTS")
if self.registrar_config
else "$ARGUMENTS"
)

# 1. Process and write command files as .agent.md
for src_file in templates:
Expand Down
1 change: 1 addition & 0 deletions src/specify_cli/integrations/devin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ def build_exec_args(
kept on the integration for tool detection.
"""
args = [self.key, "-p", prompt]
self._apply_extra_args_env_var(args)
if model:
args.extend(["--model", model])
return args
Expand Down
5 changes: 5 additions & 0 deletions src/specify_cli/integrations/opencode/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ def build_exec_args(
output_json: bool = True,
) -> list[str] | None:
args = [self.key, "run"]
# Apply operator-injected extra args before the prompt-derived
# --command and the canonical --format/-m flags so Spec Kit's
# later appends remain authoritative under repeated-flag CLI
# semantics.
self._apply_extra_args_env_var(args)

message = prompt
if prompt.startswith("/"):
Expand Down
Loading