claude-dev-env 2.18.0 → 2.20.0
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.
- package/.agents/skills/AGENTS.md +4 -4
- package/.agents/skills/_shared/AGENTS.md +1 -0
- package/.agents/skills/_shared/pr-loop/AGENTS.md +1 -0
- package/.agents/skills/_shared/pr-loop/preflight-proposal.contract.test.mjs +119 -0
- package/.agents/skills/_shared/pr-loop/preflight-proposal.md +5 -0
- package/.agents/skills/e-code-review/SKILL.md +12 -1
- package/.agents/skills/e-code-review/reference/fix.md +5 -1
- package/.agents/skills/e-code-review/reference/loop.md +4 -0
- package/.agents/skills/e-code-review/reference/mode-contract.test.mjs +66 -0
- package/.agents/skills/e-code-review/reference/preflight-proposal.md +40 -0
- package/.agents/skills/e-code-review/reference/runner-selection.md +1 -0
- package/.agents/skills/pr-cleanup/SKILL.md +105 -75
- package/.agents/skills/pr-cleanup/reference/process-inventory.md +15 -0
- package/.agents/skills/pr-cleanup/reference/task-seeds.md +20 -0
- package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/SKILL.md +18 -12
- package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/offense-examples.md +1 -1
- package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/task-seeds.md +3 -2
- package/.agents/skills/pr-refinement/SKILL.md +31 -0
- package/.agents/skills/{shared-extraction-audit → pr-shared-extraction}/SKILL.md +24 -7
- package/.agents/skills/{small-cl → pr-small-cl}/SKILL.md +1 -1
- package/.agents/skills/prototype/scripts/test_probe_sandbox_safety.py +33 -5
- package/.agents/skills/skill-builder/SKILL.md +1 -0
- package/.agents/skills-archived/bugteam/SKILL.md +1 -1
- package/.agents/skills-archived/bugteam/test_skill_additions.py +1 -1
- package/.agents/skills-archived/comments/SKILL.md +1 -1
- package/.agents/skills-archived/descriptions/SKILL.md +1 -1
- package/.agents/skills-archived/reviews/SKILL.md +1 -1
- package/AGENTS.md +1 -1
- package/_shared/pr-loop/AGENTS.md +2 -1
- package/_shared/pr-loop/preflight-proposal.md +56 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +29 -6
- package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +15 -3
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_running.py +47 -4
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +4 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +47 -0
- package/bin/ever-shipped-skills.mjs +14 -0
- package/bin/install-constants.mjs +5 -0
- package/bin/install.agents-home.test.mjs +23 -0
- package/bin/install.codex-rules.test.mjs +61 -6
- package/bin/install.mjs +57 -9
- package/bin/install.prune.test.mjs +65 -3
- package/bin/install.test.mjs +202 -2
- package/docs/CODE_RULES.md +6 -0
- package/hooks/advisory/conftest.py +10 -0
- package/hooks/advisory/migration_safety_advisor.py +14 -14
- package/hooks/advisory/refactor_guard.py +250 -144
- package/hooks/advisory/refactor_guard_test_support.py +46 -0
- package/hooks/advisory/test_migration_safety_advisor.py +120 -0
- package/hooks/advisory/test_refactor_guard_advisory.py +171 -0
- package/hooks/advisory/test_refactor_guard_eligibility.py +166 -0
- package/hooks/atomic_file_writer.py +81 -0
- package/hooks/blocking/AGENTS.md +2 -1
- package/hooks/blocking/block_main_commit.py +66 -33
- package/hooks/blocking/code_rules_banned_identifiers.py +55 -4
- package/hooks/blocking/code_rules_blast_radius.py +194 -0
- package/hooks/blocking/code_rules_enforcer.py +12 -0
- package/hooks/blocking/code_rules_enforcer_test_support.py +92 -0
- package/hooks/blocking/destructive_command_blocker.py +8 -7
- package/hooks/blocking/pre_tool_use_dispatcher.py +11 -7
- package/hooks/blocking/precommit_code_rules_gate.py +14 -139
- package/hooks/blocking/sensitive_file_protector.py +8 -6
- package/hooks/blocking/session_edit_stage_gate.py +31 -1
- package/hooks/blocking/test_block_main_commit.py +145 -0
- package/hooks/blocking/test_claude_md_orphan_file_blocker.py +171 -1
- package/hooks/blocking/test_code_rules_blast_radius.py +161 -0
- package/hooks/blocking/test_code_rules_enforcer_agent_home_tooling.py +37 -12
- package/hooks/blocking/test_code_rules_enforcer_banned_noun_word.py +54 -0
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +15 -53
- package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +13 -62
- package/hooks/blocking/test_code_rules_enforcer_narrow_edit.py +309 -0
- package/hooks/blocking/test_code_rules_enforcer_precheck_forecast.py +14 -71
- package/hooks/blocking/test_code_rules_enforcer_scratchpad.py +14 -16
- package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +31 -46
- package/hooks/blocking/test_code_rules_enforcer_stage.py +192 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +13 -59
- package/hooks/blocking/test_destructive_command_blocker.py +154 -138
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +85 -25
- package/hooks/blocking/test_destructive_command_blocker_no_verify.py +58 -38
- package/hooks/blocking/test_destructive_command_blocker_patterns.py +133 -0
- package/hooks/blocking/test_docstring_rule_gate_count_blocker.py +113 -1
- package/hooks/blocking/test_env_var_table_code_drift_blocker.py +70 -3
- package/hooks/blocking/test_hook_subprocess_support.py +296 -0
- package/hooks/blocking/test_package_inventory_stale_blocker.py +1 -1
- package/hooks/blocking/test_pii_scanner.py +81 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +32 -3
- package/hooks/blocking/test_precommit_code_rules_gate.py +11 -16
- package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +234 -0
- package/hooks/blocking/test_pytest_testpaths_orphan_blocker.py +25 -18
- package/hooks/blocking/test_sensitive_file_protector.py +145 -5
- package/hooks/blocking/test_session_edit_stage_gate_staging.py +64 -0
- package/hooks/blocking/test_session_edit_stage_gate_support.py +204 -0
- package/hooks/blocking/test_session_edit_stage_gate_tracker.py +116 -0
- package/hooks/blocking/test_shared_stdin_adoption.py +31 -20
- package/hooks/blocking/test_test_preflight_check.py +80 -0
- package/hooks/git-hooks/AGENTS.md +1 -1
- package/hooks/git-hooks/git_hooks_constants/__init__.py +1 -0
- package/hooks/git-hooks/post_commit.py +160 -51
- package/hooks/git-hooks/pre_commit.py +9 -5
- package/hooks/git-hooks/test_post_commit.py +203 -0
- package/hooks/git-hooks/test_pre_commit.py +2 -2
- package/hooks/git-hooks/test_pre_push.py +57 -0
- package/hooks/hooks_constants/AGENTS.md +3 -1
- package/hooks/hooks_constants/atomic_file_writer_constants.py +4 -0
- package/hooks/hooks_constants/banned_identifiers_constants.py +1 -0
- package/hooks/hooks_constants/blast_radius_constants.py +14 -0
- package/hooks/hooks_constants/destructive_command_environment_constants.py +18 -0
- package/hooks/hooks_constants/destructive_command_segment_constants.py +1 -11
- package/hooks/hooks_constants/hardcoded_user_path_constants.py +9 -3
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +1 -1
- package/hooks/hooks_constants/refactor_guard_constants.py +75 -0
- package/hooks/hooks_constants/sensitive_file_protector_constants.py +2 -4
- package/hooks/hooks_constants/test_refactor_guard_constants.py +21 -0
- package/hooks/json_file_reader.py +22 -0
- package/hooks/observability/test_instructions_loaded_logger.py +54 -0
- package/hooks/pending_sidecars.py +33 -0
- package/hooks/session/test_plugin_data_dir_cleanup.py +70 -0
- package/hooks/session/test_session_edit_tracker_cleanup.py +16 -3
- package/hooks/test_atomic_file_writer.py +73 -0
- package/hooks/test_json_file_reader.py +24 -0
- package/hooks/test_pending_sidecars.py +25 -0
- package/hooks/validation/mypy_validator.py +213 -80
- package/hooks/validation/test_mypy_validator.py +288 -13
- package/hooks/workflow/auto_formatter.py +225 -93
- package/hooks/workflow/investigation_tracker_reset.py +2 -0
- package/hooks/workflow/test_auto_formatter.py +261 -12
- package/hooks/workflow/test_investigation_tracker_reset.py +90 -0
- package/package.json +1 -1
- package/rules/failure-blast-radius.md +126 -0
- /package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/fetch-commands.md +0 -0
- /package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/report-template.md +0 -0
- /package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/rule-checklist.md +0 -0
- /package/.agents/skills/{shared-extraction-audit → pr-shared-extraction}/reference/examples.md +0 -0
- /package/.agents/skills/{shared-extraction-audit → pr-shared-extraction}/reference/offense-taxonomy.md +0 -0
|
@@ -10,87 +10,196 @@ When you commit in a submodule, this hook:
|
|
|
10
10
|
This prevents the "lost work" issue where submodule commits aren't tracked by parent.
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
13
15
|
import subprocess
|
|
14
16
|
import sys
|
|
17
|
+
from enum import StrEnum
|
|
15
18
|
from pathlib import Path
|
|
16
19
|
|
|
20
|
+
from git_hooks_constants import GIT_COMMAND_SUCCESS_EXIT_CODE, GIT_EXECUTABLE_NAME
|
|
17
21
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
|
|
23
|
+
class ParentPointerStatus(StrEnum):
|
|
24
|
+
"""States returned by a parent pointer update."""
|
|
25
|
+
|
|
26
|
+
UPDATED = "updated"
|
|
27
|
+
UNCHANGED = "unchanged"
|
|
28
|
+
FAILED = "failed"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
ParentPointerUpdate = tuple[ParentPointerStatus, str]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def execute_git(
|
|
35
|
+
*arguments: str,
|
|
36
|
+
cwd: Path,
|
|
37
|
+
) -> subprocess.CompletedProcess[str]:
|
|
38
|
+
"""Run a Git command and return its completed process."""
|
|
39
|
+
try:
|
|
40
|
+
return subprocess.run(
|
|
41
|
+
[GIT_EXECUTABLE_NAME, *arguments],
|
|
42
|
+
cwd=cwd,
|
|
43
|
+
check=False,
|
|
44
|
+
capture_output=True,
|
|
45
|
+
text=True,
|
|
46
|
+
)
|
|
47
|
+
except OSError as error:
|
|
48
|
+
return subprocess.CompletedProcess(
|
|
49
|
+
[GIT_EXECUTABLE_NAME, *arguments],
|
|
50
|
+
1,
|
|
51
|
+
"",
|
|
52
|
+
str(error),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def run_git(*arguments: str, cwd: Path) -> str:
|
|
57
|
+
"""Run a successful Git command and return its trimmed standard output."""
|
|
58
|
+
command_result = execute_git(*arguments, cwd=cwd)
|
|
59
|
+
if command_result.returncode != GIT_COMMAND_SUCCESS_EXIT_CODE:
|
|
60
|
+
return ""
|
|
61
|
+
return command_result.stdout.strip()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def run_git_from_current_directory(*arguments: str) -> str:
|
|
65
|
+
"""Run a successful Git command from the hook's current directory."""
|
|
66
|
+
return run_git(*arguments, cwd=Path.cwd())
|
|
27
67
|
|
|
28
68
|
|
|
29
69
|
def find_parent_repo(repo_dir: Path) -> Path | None:
|
|
30
|
-
"""
|
|
31
|
-
|
|
32
|
-
|
|
70
|
+
"""Return the Git superproject that owns the current repository."""
|
|
71
|
+
parent_path_text = run_git(
|
|
72
|
+
"rev-parse",
|
|
73
|
+
"--show-superproject-working-tree",
|
|
74
|
+
cwd=repo_dir,
|
|
75
|
+
)
|
|
76
|
+
if not parent_path_text:
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
parent_path = Path(parent_path_text).resolve()
|
|
80
|
+
if not parent_path.is_dir():
|
|
81
|
+
return None
|
|
82
|
+
return parent_path
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def find_submodule_path(parent_repo: Path, repo_dir: Path) -> Path | None:
|
|
86
|
+
"""Return the submodule path relative to its parent repository."""
|
|
87
|
+
try:
|
|
88
|
+
return repo_dir.resolve().relative_to(parent_repo.resolve())
|
|
89
|
+
except ValueError:
|
|
90
|
+
return None
|
|
91
|
+
|
|
33
92
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
93
|
+
def build_literal_pathspec(submodule_path: Path) -> str:
|
|
94
|
+
"""Build a Git pathspec that treats every path character literally."""
|
|
95
|
+
return f":(literal){submodule_path.as_posix()}"
|
|
37
96
|
|
|
38
|
-
if git_path.exists() and gitmodules_path.exists():
|
|
39
|
-
try:
|
|
40
|
-
content = gitmodules_path.read_text()
|
|
41
|
-
if f"path = {repo_name}" in content:
|
|
42
|
-
return parent_dir
|
|
43
|
-
except Exception:
|
|
44
|
-
pass
|
|
45
97
|
|
|
46
|
-
|
|
98
|
+
def get_git_failure_diagnostic(
|
|
99
|
+
command_result: subprocess.CompletedProcess[str],
|
|
100
|
+
) -> str:
|
|
101
|
+
"""Return the most useful diagnostic from a failed Git command."""
|
|
102
|
+
return command_result.stderr.strip() or command_result.stdout.strip()
|
|
47
103
|
|
|
48
|
-
|
|
104
|
+
|
|
105
|
+
def build_parent_commit_message(
|
|
106
|
+
repo_name: str,
|
|
107
|
+
commit_hash: str,
|
|
108
|
+
commit_message: str,
|
|
109
|
+
) -> str:
|
|
110
|
+
"""Build the parent commit message for a submodule pointer update."""
|
|
111
|
+
return (
|
|
112
|
+
f"chore: update {repo_name} submodule to {commit_hash}\n\n"
|
|
113
|
+
f"Submodule commit: {commit_message}\n\n"
|
|
114
|
+
"Co-Authored-By: Claude <noreply@anthropic.com>"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def update_parent_pointer(
|
|
119
|
+
parent_repo: Path,
|
|
120
|
+
repo_dir: Path,
|
|
121
|
+
commit_hash: str,
|
|
122
|
+
commit_message: str,
|
|
123
|
+
) -> ParentPointerUpdate:
|
|
124
|
+
"""Commit the current submodule pointer in its parent repository."""
|
|
125
|
+
submodule_path = find_submodule_path(parent_repo, repo_dir)
|
|
126
|
+
if submodule_path is None:
|
|
127
|
+
return ParentPointerStatus.FAILED, "could not resolve the submodule path"
|
|
128
|
+
|
|
129
|
+
submodule_reference = build_literal_pathspec(submodule_path)
|
|
130
|
+
add_result = execute_git("add", "--", submodule_reference, cwd=parent_repo)
|
|
131
|
+
if add_result.returncode != GIT_COMMAND_SUCCESS_EXIT_CODE:
|
|
132
|
+
return ParentPointerStatus.FAILED, get_git_failure_diagnostic(add_result)
|
|
133
|
+
|
|
134
|
+
staged_difference = execute_git(
|
|
135
|
+
"diff",
|
|
136
|
+
"--cached",
|
|
137
|
+
"--quiet",
|
|
138
|
+
"--",
|
|
139
|
+
submodule_reference,
|
|
140
|
+
cwd=parent_repo,
|
|
141
|
+
)
|
|
142
|
+
if staged_difference.returncode == GIT_COMMAND_SUCCESS_EXIT_CODE:
|
|
143
|
+
return ParentPointerStatus.UNCHANGED, ""
|
|
144
|
+
if staged_difference.returncode != 1:
|
|
145
|
+
return ParentPointerStatus.FAILED, get_git_failure_diagnostic(staged_difference)
|
|
146
|
+
|
|
147
|
+
parent_commit_message = build_parent_commit_message(
|
|
148
|
+
repo_dir.name,
|
|
149
|
+
commit_hash,
|
|
150
|
+
commit_message,
|
|
151
|
+
)
|
|
152
|
+
commit_result = execute_git(
|
|
153
|
+
"commit",
|
|
154
|
+
"--only",
|
|
155
|
+
"-m",
|
|
156
|
+
parent_commit_message,
|
|
157
|
+
"--",
|
|
158
|
+
submodule_reference,
|
|
159
|
+
cwd=parent_repo,
|
|
160
|
+
)
|
|
161
|
+
if commit_result.returncode != GIT_COMMAND_SUCCESS_EXIT_CODE:
|
|
162
|
+
return ParentPointerStatus.FAILED, get_git_failure_diagnostic(commit_result)
|
|
163
|
+
return ParentPointerStatus.UPDATED, ""
|
|
49
164
|
|
|
50
165
|
|
|
51
166
|
def main() -> int:
|
|
52
|
-
"""
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
except Exception:
|
|
167
|
+
"""Update a parent repository after a submodule commit."""
|
|
168
|
+
repo_path_text = run_git_from_current_directory("rev-parse", "--show-toplevel")
|
|
169
|
+
if not repo_path_text:
|
|
56
170
|
return 0
|
|
57
171
|
|
|
58
|
-
|
|
172
|
+
repo_dir = Path(repo_path_text).resolve()
|
|
59
173
|
parent_repo = find_parent_repo(repo_dir)
|
|
60
|
-
|
|
61
|
-
if not parent_repo:
|
|
174
|
+
if parent_repo is None:
|
|
62
175
|
return 0
|
|
63
176
|
|
|
64
177
|
commit_msg = run_git("log", "-1", "--pretty=%s", cwd=repo_dir)
|
|
65
|
-
commit_hash = run_git("rev-parse", "
|
|
178
|
+
commit_hash = run_git("rev-parse", "HEAD", cwd=repo_dir)
|
|
179
|
+
short_commit_hash = run_git("rev-parse", "--short", "HEAD", cwd=repo_dir)
|
|
180
|
+
if not commit_hash or not short_commit_hash:
|
|
181
|
+
return 0
|
|
66
182
|
|
|
67
183
|
print()
|
|
68
184
|
print("=== Submodule Parent Update ===")
|
|
69
|
-
print(f"Submodule: {
|
|
185
|
+
print(f"Submodule: {repo_dir.name} @ {short_commit_hash}")
|
|
70
186
|
print(f"Parent: {parent_repo}")
|
|
71
187
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
188
|
+
parent_pointer_status, parent_pointer_diagnostic = update_parent_pointer(
|
|
189
|
+
parent_repo,
|
|
190
|
+
repo_dir,
|
|
191
|
+
commit_hash,
|
|
192
|
+
commit_msg,
|
|
77
193
|
)
|
|
78
|
-
|
|
79
|
-
|
|
194
|
+
if parent_pointer_status is ParentPointerStatus.FAILED:
|
|
195
|
+
print("Parent update failed.")
|
|
196
|
+
if parent_pointer_diagnostic:
|
|
197
|
+
print(f"Git diagnostic: {parent_pointer_diagnostic}")
|
|
198
|
+
return 0
|
|
199
|
+
if parent_pointer_status is ParentPointerStatus.UNCHANGED:
|
|
80
200
|
print("Parent already up to date.")
|
|
81
201
|
return 0
|
|
82
202
|
|
|
83
|
-
full_commit_msg = f"""chore: update {repo_name} submodule to {commit_hash}
|
|
84
|
-
|
|
85
|
-
Submodule commit: {commit_msg}
|
|
86
|
-
|
|
87
|
-
Co-Authored-By: Claude <noreply@anthropic.com>"""
|
|
88
|
-
|
|
89
|
-
subprocess.run(
|
|
90
|
-
["git", "commit", "-m", full_commit_msg],
|
|
91
|
-
check=False, cwd=parent_repo,
|
|
92
|
-
)
|
|
93
|
-
|
|
94
203
|
print("Parent updated successfully.")
|
|
95
204
|
print("================================")
|
|
96
205
|
print()
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Git pre-commit
|
|
2
|
+
"""Native Git pre-commit owner for staged CODE_RULES validation.
|
|
3
3
|
|
|
4
4
|
Installed to the user's shared git-hooks directory via the claude-dev-env
|
|
5
5
|
installer; git invokes this file as `pre-commit` (the installer strips the
|
|
6
6
|
`_` and `.py` suffix when copying into the live hooks path).
|
|
7
7
|
|
|
8
|
+
The Agent Bash commit surface records passive staged-surface evidence. This
|
|
9
|
+
native hook invokes the shared gate and owns the staged commit decision.
|
|
10
|
+
|
|
8
11
|
Exit codes:
|
|
9
12
|
0 - staged changes pass the gate (or the gate is not installed locally).
|
|
10
13
|
1 - staged changes introduce one or more blocking violations.
|
|
@@ -17,23 +20,24 @@ import subprocess
|
|
|
17
20
|
import sys
|
|
18
21
|
from pathlib import Path
|
|
19
22
|
|
|
23
|
+
from gate_utils import is_safe_regular_file, resolve_gate_script_path
|
|
20
24
|
from git_hooks_constants import (
|
|
21
25
|
GATE_INFRASTRUCTURE_FAILURE_EXIT_CODE,
|
|
22
26
|
GATE_SCRIPT_NOT_FOUND_MESSAGE,
|
|
27
|
+
IMMEDIATE_SCOPE_ARGUMENT,
|
|
23
28
|
INVOKE_GATE_FAILURE_MESSAGE,
|
|
24
|
-
STAGED_SCOPE_ARGUMENT,
|
|
25
29
|
)
|
|
26
|
-
from gate_utils import is_safe_regular_file, resolve_gate_script_path
|
|
27
30
|
|
|
28
31
|
|
|
29
32
|
def invoke_gate(gate_script_path: Path) -> int:
|
|
30
|
-
|
|
33
|
+
"""Invoke the shared staged gate and return its exit code."""
|
|
34
|
+
immediate_scope_argument = IMMEDIATE_SCOPE_ARGUMENT
|
|
31
35
|
invoke_gate_failure_message = INVOKE_GATE_FAILURE_MESSAGE
|
|
32
36
|
gate_infrastructure_failure_exit_code = GATE_INFRASTRUCTURE_FAILURE_EXIT_CODE
|
|
33
37
|
try:
|
|
34
38
|
resolved_gate_path = gate_script_path.resolve(strict=True)
|
|
35
39
|
completion = subprocess.run(
|
|
36
|
-
[sys.executable, str(resolved_gate_path),
|
|
40
|
+
[sys.executable, str(resolved_gate_path), immediate_scope_argument],
|
|
37
41
|
check=False,
|
|
38
42
|
)
|
|
39
43
|
except OSError as launch_error:
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import post_commit
|
|
8
|
+
import pytest
|
|
9
|
+
from git_hooks_constants import GIT_EXECUTABLE_NAME
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def build_fixture_git_environment() -> dict[str, str]:
|
|
13
|
+
"""Copy the process environment without inherited Git state overrides."""
|
|
14
|
+
return {
|
|
15
|
+
each_name: each_value
|
|
16
|
+
for each_name, each_value in os.environ.items()
|
|
17
|
+
if not each_name.upper().startswith("GIT_")
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def clear_inherited_git_environment(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
22
|
+
"""Remove inherited Git state overrides from the current test process."""
|
|
23
|
+
for each_name in list(os.environ):
|
|
24
|
+
if each_name.upper().startswith("GIT_"):
|
|
25
|
+
monkeypatch.delenv(each_name, raising=False)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_fixture_git_environment_preserves_process_execution_path(
|
|
29
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
30
|
+
) -> None:
|
|
31
|
+
"""The fixture environment keeps PATH while removing Git overrides."""
|
|
32
|
+
monkeypatch.setenv("PATH", "fixture-execution-path")
|
|
33
|
+
monkeypatch.setenv("GIT_DIR", "unrelated-repository")
|
|
34
|
+
|
|
35
|
+
fixture_environment = build_fixture_git_environment()
|
|
36
|
+
|
|
37
|
+
assert fixture_environment["PATH"] == "fixture-execution-path"
|
|
38
|
+
assert all(not each_name.upper().startswith("GIT_") for each_name in fixture_environment)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def run_fixture_git(repository_path: Path, *arguments: str) -> str:
|
|
42
|
+
"""Run Git in a fixture repository and return standard output."""
|
|
43
|
+
completed_process = subprocess.run(
|
|
44
|
+
[GIT_EXECUTABLE_NAME, *arguments],
|
|
45
|
+
cwd=repository_path,
|
|
46
|
+
check=False,
|
|
47
|
+
capture_output=True,
|
|
48
|
+
text=True,
|
|
49
|
+
env=build_fixture_git_environment(),
|
|
50
|
+
)
|
|
51
|
+
assert completed_process.returncode == 0, completed_process.stderr
|
|
52
|
+
return completed_process.stdout.strip()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def configure_fixture_identity(repository_path: Path) -> None:
|
|
56
|
+
"""Configure a stable identity for a fixture repository."""
|
|
57
|
+
run_fixture_git(repository_path, "config", "user.name", "Fixture User")
|
|
58
|
+
run_fixture_git(
|
|
59
|
+
repository_path,
|
|
60
|
+
"config",
|
|
61
|
+
"user.email",
|
|
62
|
+
"fixture@example.invalid",
|
|
63
|
+
)
|
|
64
|
+
run_fixture_git(repository_path, "config", "commit.gpgsign", "false")
|
|
65
|
+
fixture_hooks_directory = repository_path / ".fixture-hooks"
|
|
66
|
+
fixture_hooks_directory.mkdir(exist_ok=True)
|
|
67
|
+
run_fixture_git(
|
|
68
|
+
repository_path,
|
|
69
|
+
"config",
|
|
70
|
+
"core.hooksPath",
|
|
71
|
+
str(fixture_hooks_directory),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def initialize_fixture_repository(repository_path: Path) -> None:
|
|
76
|
+
"""Create and configure a fixture Git repository."""
|
|
77
|
+
repository_path.mkdir()
|
|
78
|
+
run_fixture_git(repository_path, "init", "-b", "main")
|
|
79
|
+
configure_fixture_identity(repository_path)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_native_submodule_commit_records_parent_pointer(
|
|
83
|
+
tmp_path: Path,
|
|
84
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
85
|
+
) -> None:
|
|
86
|
+
"""A real submodule commit creates the matching parent pointer commit."""
|
|
87
|
+
unrelated_repository = tmp_path / "unrelated-repository"
|
|
88
|
+
initialize_fixture_repository(unrelated_repository)
|
|
89
|
+
monkeypatch.setenv("GIT_DIR", str(unrelated_repository / ".git"))
|
|
90
|
+
|
|
91
|
+
source_repository = tmp_path / "source-repository"
|
|
92
|
+
parent_repository = tmp_path / "parent-repository"
|
|
93
|
+
initialize_fixture_repository(source_repository)
|
|
94
|
+
(source_repository / "README.md").write_text("fixture source\n", encoding="utf-8")
|
|
95
|
+
run_fixture_git(source_repository, "add", "README.md")
|
|
96
|
+
run_fixture_git(source_repository, "commit", "-m", "Create fixture source")
|
|
97
|
+
|
|
98
|
+
initialize_fixture_repository(parent_repository)
|
|
99
|
+
(parent_repository / "README.md").write_text("fixture parent\n", encoding="utf-8")
|
|
100
|
+
run_fixture_git(parent_repository, "add", "README.md")
|
|
101
|
+
run_fixture_git(parent_repository, "commit", "-m", "Create fixture parent")
|
|
102
|
+
run_fixture_git(
|
|
103
|
+
parent_repository,
|
|
104
|
+
"-c",
|
|
105
|
+
"protocol.file.allow=always",
|
|
106
|
+
"submodule",
|
|
107
|
+
"add",
|
|
108
|
+
str(source_repository),
|
|
109
|
+
"nested/sub[module]",
|
|
110
|
+
)
|
|
111
|
+
run_fixture_git(parent_repository, "commit", "-am", "Add fixture submodule")
|
|
112
|
+
|
|
113
|
+
submodule_repository = parent_repository / "nested" / "sub[module]"
|
|
114
|
+
configure_fixture_identity(submodule_repository)
|
|
115
|
+
(parent_repository / "staged-note.txt").write_text("keep staged\n", encoding="utf-8")
|
|
116
|
+
run_fixture_git(parent_repository, "add", "staged-note.txt")
|
|
117
|
+
(submodule_repository / "change.txt").write_text("fixture change\n", encoding="utf-8")
|
|
118
|
+
run_fixture_git(submodule_repository, "add", "change.txt")
|
|
119
|
+
run_fixture_git(submodule_repository, "commit", "-m", "Record fixture change")
|
|
120
|
+
clear_inherited_git_environment(monkeypatch)
|
|
121
|
+
monkeypatch.chdir(submodule_repository)
|
|
122
|
+
assert post_commit.main() == 0
|
|
123
|
+
|
|
124
|
+
submodule_commit_hash = run_fixture_git(submodule_repository, "rev-parse", "HEAD")
|
|
125
|
+
parent_commit_subject = run_fixture_git(parent_repository, "log", "-1", "--pretty=%s")
|
|
126
|
+
parent_commit_body = run_fixture_git(parent_repository, "log", "-1", "--pretty=%B")
|
|
127
|
+
recorded_pointer_hash = run_fixture_git(
|
|
128
|
+
parent_repository,
|
|
129
|
+
"rev-parse",
|
|
130
|
+
"HEAD:nested/sub[module]",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
assert recorded_pointer_hash == submodule_commit_hash
|
|
134
|
+
assert parent_commit_subject == (
|
|
135
|
+
f"chore: update sub[module] submodule to {submodule_commit_hash}"
|
|
136
|
+
)
|
|
137
|
+
assert "Submodule commit: Record fixture change" in parent_commit_body
|
|
138
|
+
assert run_fixture_git(parent_repository, "status", "--short") == "A staged-note.txt"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_parent_update_returns_git_diagnostic_for_failed_add(
|
|
142
|
+
tmp_path: Path,
|
|
143
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
144
|
+
) -> None:
|
|
145
|
+
"""A failed parent add returns the Git diagnostic and literal pathspec."""
|
|
146
|
+
parent_repository = tmp_path / "parent-repository"
|
|
147
|
+
submodule_repository = parent_repository / "sub[module]"
|
|
148
|
+
submodule_repository.mkdir(parents=True)
|
|
149
|
+
recorded_commands: list[tuple[str, ...]] = []
|
|
150
|
+
|
|
151
|
+
def fail_git_add(*arguments: str, cwd: Path) -> subprocess.CompletedProcess[str]:
|
|
152
|
+
recorded_commands.append(arguments)
|
|
153
|
+
return subprocess.CompletedProcess(
|
|
154
|
+
["git", *arguments],
|
|
155
|
+
1,
|
|
156
|
+
"",
|
|
157
|
+
"fatal: fixture add failure",
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
monkeypatch.setattr(post_commit, "execute_git", fail_git_add)
|
|
161
|
+
|
|
162
|
+
parent_update = post_commit.update_parent_pointer(
|
|
163
|
+
parent_repository,
|
|
164
|
+
submodule_repository,
|
|
165
|
+
"a" * 40,
|
|
166
|
+
"Fixture commit",
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
assert parent_update == (
|
|
170
|
+
post_commit.ParentPointerStatus.FAILED,
|
|
171
|
+
"fatal: fixture add failure",
|
|
172
|
+
)
|
|
173
|
+
assert recorded_commands == [("add", "--", ":(literal)sub[module]")]
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_main_prints_git_failure_diagnostic(
|
|
177
|
+
tmp_path: Path,
|
|
178
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
179
|
+
capsys: pytest.CaptureFixture[str],
|
|
180
|
+
) -> None:
|
|
181
|
+
"""The hook prints the Git diagnostic when the parent update fails."""
|
|
182
|
+
submodule_repository = tmp_path / "submodule"
|
|
183
|
+
parent_repository = tmp_path / "parent"
|
|
184
|
+
submodule_repository.mkdir()
|
|
185
|
+
parent_repository.mkdir()
|
|
186
|
+
monkeypatch.setattr(
|
|
187
|
+
post_commit,
|
|
188
|
+
"run_git_from_current_directory",
|
|
189
|
+
lambda *arguments: str(submodule_repository),
|
|
190
|
+
)
|
|
191
|
+
monkeypatch.setattr(post_commit, "find_parent_repo", lambda repo: parent_repository)
|
|
192
|
+
monkeypatch.setattr(post_commit, "run_git", lambda *arguments, cwd: "a" * 40)
|
|
193
|
+
monkeypatch.setattr(
|
|
194
|
+
post_commit,
|
|
195
|
+
"update_parent_pointer",
|
|
196
|
+
lambda *arguments: (
|
|
197
|
+
post_commit.ParentPointerStatus.FAILED,
|
|
198
|
+
"fatal: fixture commit failure",
|
|
199
|
+
),
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
assert post_commit.main() == 0
|
|
203
|
+
assert "Git diagnostic: fatal: fixture commit failure" in capsys.readouterr().out
|
|
@@ -60,7 +60,7 @@ def test_main_propagates_passing_exit_code_from_gate(
|
|
|
60
60
|
assert exit_code == 0
|
|
61
61
|
|
|
62
62
|
|
|
63
|
-
def
|
|
63
|
+
def test_main_invokes_gate_with_immediate_flag(
|
|
64
64
|
tmp_path: Path,
|
|
65
65
|
monkeypatch: pytest.MonkeyPatch,
|
|
66
66
|
) -> None:
|
|
@@ -84,7 +84,7 @@ def test_main_invokes_gate_with_staged_flag(
|
|
|
84
84
|
recorded_arguments = recorded_arguments_path.read_text(
|
|
85
85
|
encoding="utf-8"
|
|
86
86
|
).splitlines()
|
|
87
|
-
assert recorded_arguments == ["--
|
|
87
|
+
assert recorded_arguments == ["--immediate"]
|
|
88
88
|
|
|
89
89
|
|
|
90
90
|
def test_main_exits_two_when_invoke_gate_raises_oserror(
|
|
@@ -1567,6 +1567,63 @@ def test_main_reads_default_branch_tracking_reference_when_origin_head_is_unset(
|
|
|
1567
1567
|
assert exit_code == 0
|
|
1568
1568
|
|
|
1569
1569
|
|
|
1570
|
+
def test_main_selects_stdin_bases_for_new_and_existing_branch_pushes(
|
|
1571
|
+
tmp_path: Path,
|
|
1572
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1573
|
+
) -> None:
|
|
1574
|
+
repository, default_branch_tip, topic_branch_tip = _build_default_branch_repository(
|
|
1575
|
+
tmp_path
|
|
1576
|
+
)
|
|
1577
|
+
recorded_arguments_path = tmp_path / "recorded_arguments.txt"
|
|
1578
|
+
recording_gate_script_path = tmp_path / "recording_gate.py"
|
|
1579
|
+
recording_gate_script_path.write_text(
|
|
1580
|
+
"import pathlib, sys\n"
|
|
1581
|
+
f'pathlib.Path(r"{recorded_arguments_path}").write_text('
|
|
1582
|
+
"'\\n'.join(sys.argv[1:]), encoding='utf-8')\n"
|
|
1583
|
+
"sys.exit(0)\n",
|
|
1584
|
+
encoding="utf-8",
|
|
1585
|
+
)
|
|
1586
|
+
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(recording_gate_script_path))
|
|
1587
|
+
monkeypatch.chdir(repository)
|
|
1588
|
+
monkeypatch.setattr(
|
|
1589
|
+
sys,
|
|
1590
|
+
"stdin",
|
|
1591
|
+
io.StringIO(
|
|
1592
|
+
_push_stdin_line(
|
|
1593
|
+
FEATURE_BRANCH_NAME,
|
|
1594
|
+
topic_branch_tip,
|
|
1595
|
+
FEATURE_BRANCH_NAME,
|
|
1596
|
+
ALL_ZEROS_OBJECT_NAME,
|
|
1597
|
+
)
|
|
1598
|
+
),
|
|
1599
|
+
)
|
|
1600
|
+
|
|
1601
|
+
assert pre_push.main() == 0
|
|
1602
|
+
assert recorded_arguments_path.read_text(encoding="utf-8").splitlines() == [
|
|
1603
|
+
"--base",
|
|
1604
|
+
git_hooks_constants.DEFAULT_REMOTE_BASE_REFERENCE,
|
|
1605
|
+
]
|
|
1606
|
+
|
|
1607
|
+
monkeypatch.setattr(
|
|
1608
|
+
sys,
|
|
1609
|
+
"stdin",
|
|
1610
|
+
io.StringIO(
|
|
1611
|
+
_push_stdin_line(
|
|
1612
|
+
FEATURE_BRANCH_NAME,
|
|
1613
|
+
topic_branch_tip,
|
|
1614
|
+
FEATURE_BRANCH_NAME,
|
|
1615
|
+
NON_ZERO_REMOTE_SHA_ONE,
|
|
1616
|
+
)
|
|
1617
|
+
),
|
|
1618
|
+
)
|
|
1619
|
+
|
|
1620
|
+
assert pre_push.main() == 0
|
|
1621
|
+
assert recorded_arguments_path.read_text(encoding="utf-8").splitlines() == [
|
|
1622
|
+
"--base",
|
|
1623
|
+
default_branch_tip,
|
|
1624
|
+
]
|
|
1625
|
+
|
|
1626
|
+
|
|
1570
1627
|
def test_main_skips_a_deletion_line_to_reach_the_branch_update(
|
|
1571
1628
|
tmp_path: Path,
|
|
1572
1629
|
monkeypatch: pytest.MonkeyPatch,
|
|
@@ -11,6 +11,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
11
11
|
| `ask_user_question_shape.py` | Pure AskUserQuestion lean-block shape analyzer: grades question and option-description prose against shared chat-detail markers and sentence/word caps with no hook I/O |
|
|
12
12
|
| `banned_identifiers_constants.py` | The set of banned short identifiers and banned function-name prefixes |
|
|
13
13
|
| `bash_pre_tool_use_dispatcher_constants.py` | Permission outcomes, tool-name sets, and the ordered hosted-hook roster for the Bash PreToolUse dispatcher (also covers PowerShell-shared gates) |
|
|
14
|
+
| `pre_tool_use_dispatcher_constants.py` | Shared Write, Edit, and MultiEdit tool-name set and the ordered hosted-hook roster for the Write/Edit/MultiEdit PreToolUse dispatcher |
|
|
14
15
|
| `blocking_check_limits.py` | Max issue counts and preview lengths for blocking hooks |
|
|
15
16
|
| `bot_mention_comment_blocker_constants.py` | Patterns for detecting bot @-mentions in PR comments |
|
|
16
17
|
| `claude_md_orphan_file_blocker_constants.py` | Table patterns, file extensions, scan budget, and block-message text for the CLAUDE.md orphan-file blocker |
|
|
@@ -24,6 +25,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
24
25
|
| `dead_config_field_constants.py` | Patterns for detecting unused `*Config` / `*Selectors` dataclass fields |
|
|
25
26
|
| `dead_dataclass_field_constants.py` | Patterns for detecting unused dataclass fields |
|
|
26
27
|
| `dead_module_constant_constants.py` | Patterns for detecting unexported `UPPER_SNAKE` constants in `*_constants.py` modules |
|
|
28
|
+
| `destructive_command_environment_constants.py` | Environment keys and shared truthy values used by destructive-command hooks |
|
|
27
29
|
| `destructive_command_segment_constants.py` | The list of destructive shell command patterns the blocker matches |
|
|
28
30
|
| `docstring_rule_gate_count_blocker_constants.py` | Target rule basename, spelled-out-number lookup, count-clause and `check_*` validator patterns, and block-message text for the docstring-rule gate-count staleness blocker |
|
|
29
31
|
| `duplicate_function_body_constants.py` | Hashing and comparison config for the duplicate-body check |
|
|
@@ -62,7 +64,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
62
64
|
| `python_style_checks_constants.py` | Command-line argument count and blank-line count between top-level functions for the style validator |
|
|
63
65
|
| `ruff_integration_constants.py` | ``NO_COLOR`` / ``FORCE_COLOR`` environment variable names for plain ruff diagnostics |
|
|
64
66
|
| `send_user_file_open_locally_blocker_constants.py` | Tool name, proactive status, and the block message for the open-locally attach blocker |
|
|
65
|
-
| `sensitive_file_protector_constants.py` | Sensitive filename patterns,
|
|
67
|
+
| `sensitive_file_protector_constants.py` | Sensitive filename patterns, committed-template suffixes that earn an exemption, and the deny decision and message template for `sensitive_file_protector` |
|
|
66
68
|
| `session_edit_stage_gate_constants.py` | Tracker filename prefix/suffix, JSON payload key, edit tool name set, session-id sanitize pattern, lock filename suffix and lock-acquire timing, git diff command, commit flag escapes, and deny-message template shared by the session edit stage gate trio |
|
|
67
69
|
| `session_env_cleanup_constants.py` | Stale-age threshold and directory names for the session-env cleanup hook |
|
|
68
70
|
| `session_start_injector.py` | Pure SessionStart context injector: normalize `source` (`startup`/`resume`/`clear`/`compact`), honor enable/timeout, return structured injection results for starter hooks |
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Constants for the blast-radius declaration check."""
|
|
2
|
+
|
|
3
|
+
BLAST_RADIUS_RUN_SUFFIX = "RunFatal"
|
|
4
|
+
BLAST_RADIUS_ITEM_SUFFIX = "ItemBlocked"
|
|
5
|
+
ALL_BLAST_RADIUS_SUFFIXES = (BLAST_RADIUS_RUN_SUFFIX, BLAST_RADIUS_ITEM_SUFFIX)
|
|
6
|
+
|
|
7
|
+
MAX_BLAST_RADIUS_ISSUES = 20
|
|
8
|
+
|
|
9
|
+
BLAST_RADIUS_MESSAGE_SUFFIX = (
|
|
10
|
+
"raises inside per-item work; name its blast radius by ending the type in "
|
|
11
|
+
f"{BLAST_RADIUS_RUN_SUFFIX} when the whole run stops, or {BLAST_RADIUS_ITEM_SUFFIX} "
|
|
12
|
+
"when this one item stops and the batch carries on. See "
|
|
13
|
+
"rules/failure-blast-radius.md."
|
|
14
|
+
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Environment keys and values used by destructive-command hooks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
ALL_KNOWN_TEMPORARY_ENVIRONMENT_VARIABLE_NAMES: frozenset[str] = frozenset(
|
|
6
|
+
{
|
|
7
|
+
"TEMP",
|
|
8
|
+
"TMP",
|
|
9
|
+
"TMPDIR",
|
|
10
|
+
"CLAUDE_JOB_DIR",
|
|
11
|
+
}
|
|
12
|
+
)
|
|
13
|
+
ALL_TRUTHY_ENV_VALUES: frozenset[str] = frozenset({"1", "true", "yes", "on"})
|
|
14
|
+
DESTRUCTIVE_DENY_MODE_ENV_VAR: str = "CLAUDE_DESTRUCTIVE_DENY_MODE"
|
|
15
|
+
EPHEMERAL_AUTO_ALLOW_DISABLE_ENV_VAR: str = (
|
|
16
|
+
"CLAUDE_DESTRUCTIVE_DISABLE_EPHEMERAL_AUTO_ALLOW"
|
|
17
|
+
)
|
|
18
|
+
GH_REDIRECT_ACTIVE_ENV_VAR: str = "CLAUDE_GH_REDIRECT_ACTIVE"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Segment-splitting
|
|
1
|
+
"""Segment-splitting and command-name constants for the destructive command blocker."""
|
|
2
2
|
|
|
3
3
|
ALL_SHELL_CONTROL_OPERATOR_TOKENS: frozenset[str] = frozenset({"&&", "||", ";", "|&", "|", "&", "\n", "\r"})
|
|
4
4
|
ALL_COMMAND_LAUNCHER_WRAPPER_COMMANDS: frozenset[str] = frozenset(
|
|
@@ -183,13 +183,3 @@ ALL_LAUNCHER_OPTIONS_TAKING_SEPARATE_VALUE: frozenset[str] = frozenset(
|
|
|
183
183
|
}
|
|
184
184
|
)
|
|
185
185
|
ALL_SUBSHELL_GROUPING_CHARACTERS: str = "({"
|
|
186
|
-
ALL_KNOWN_TEMPORARY_ENVIRONMENT_VARIABLE_NAMES: frozenset[str] = frozenset(
|
|
187
|
-
{
|
|
188
|
-
"TEMP",
|
|
189
|
-
"TMP",
|
|
190
|
-
"TMPDIR",
|
|
191
|
-
"CLAUDE_JOB_DIR",
|
|
192
|
-
}
|
|
193
|
-
)
|
|
194
|
-
DESTRUCTIVE_DENY_MODE_ENV_VAR: str = "CLAUDE_DESTRUCTIVE_DENY_MODE"
|
|
195
|
-
ALL_TRUTHY_ENV_VALUES: frozenset[str] = frozenset({"1", "true", "yes", "on"})
|