claude-dev-env 7.4.0 → 7.5.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/bin/install.mjs +0 -1
- package/bin/install.policy-lint.test.mjs +1 -2
- package/bin/install.test.mjs +4 -4
- package/bin/pstack.mjs +24 -4
- package/bin/pstack.test.mjs +20 -10
- package/hooks/advisory/msys_path_conversion_advisor.py +200 -0
- package/hooks/advisory/pr_done_reminder.py +7 -13
- package/hooks/advisory/test_msys_path_conversion_advisor.py +223 -0
- package/hooks/advisory/test_pr_done_reminder.py +1 -0
- package/hooks/blocking/bash_post_call_dispatcher.py +6 -11
- package/hooks/blocking/env_var_table_code_drift_blocker.py +11 -99
- package/hooks/blocking/env_var_table_rows.py +130 -0
- package/hooks/blocking/msys_rev_path_rewriter.py +192 -0
- package/hooks/blocking/pii_scanner.py +19 -29
- package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +16 -3
- package/hooks/blocking/test_bash_post_call_dispatcher.py +2 -0
- package/hooks/blocking/test_env_var_table_code_drift_blocker.py +81 -3
- package/hooks/blocking/test_env_var_table_rows.py +100 -0
- package/hooks/blocking/test_msys_rev_path_rewriter.py +141 -0
- package/hooks/blocking/test_pii_scanner.py +34 -24
- package/hooks/hooks.json +10 -0
- package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +9 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +6 -1
- package/hooks/hooks_constants/env_var_table_code_drift_constants.py +12 -4
- package/hooks/hooks_constants/hardcoded_user_path_constants.py +12 -10
- package/hooks/hooks_constants/msys_path_conversion_advisor_constants.py +48 -0
- package/hooks/hooks_constants/msys_rev_path_rewriter_constants.py +37 -0
- package/hooks/hooks_constants/pii_prevention_constants.py +10 -25
- package/hooks/hooks_constants/post_tool_use_context.py +34 -0
- package/hooks/hooks_constants/pr_done_reminder_constants.py +0 -2
- package/hooks/hooks_constants/pre_tool_use_allow_output.py +48 -0
- package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +9 -2
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +9 -2
- package/hooks/hooks_constants/test_failure_recorder_constants.py +1 -2
- package/hooks/hooks_constants/test_post_tool_use_context.py +22 -0
- package/hooks/hooks_constants/test_pre_tool_use_allow_output.py +27 -0
- package/hooks/observability/test_failure_recorder.py +3 -1
- package/package.json +1 -1
- package/rules/ask-user-question-required.md +3 -3
- package/rules/code-standards.md +3 -2
- package/rules/confirm-implementation-forks.md +2 -0
- package/rules/destructive-commands.md +19 -19
- package/rules/doc-inventory-integrity.md +4 -4
- package/rules/docstring-prose-matches-implementation.md +2 -2
- package/rules/failure-blast-radius.md +4 -2
- package/rules/filesystem-search.md +1 -1
- package/rules/git-workflow.md +34 -0
- package/rules/long-horizon-autonomy.md +2 -0
- package/rules/nas-ssh-invocation.md +3 -3
- package/rules/no-cross-skill-duplicate-helpers.md +6 -6
- package/rules/orphan-css-class.md +5 -5
- package/rules/paired-test-coverage.md +6 -6
- package/rules/plain-illustrative-docstrings.md +3 -3
- package/rules/re-stage-before-commit.md +7 -7
- package/rules/retired-hook-prose.md +16 -0
- package/rules/shell-invocation.md +17 -1
- package/rules/testing.md +3 -5
- package/rules/verify-runtime-state.md +1 -1
- package/rules/windows-filesystem-safe.md +2 -2
- package/rules/workflow-substitution-slots.md +1 -1
- package/scripts/durable_post_lint_config/config/constants.py +0 -3
- package/scripts/durable_post_lint_config/config/test_constants.py +5 -0
- package/scripts/policy_lint/adapter_configuration.py +24 -0
- package/scripts/policy_lint/adapter_retired_hook_prose.py +201 -0
- package/scripts/policy_lint/adapters.py +36 -1
- package/scripts/policy_lint/config/constants.py +74 -0
- package/scripts/policy_lint/registry.py +5 -0
- package/scripts/policy_lint/selection.py +37 -18
- package/scripts/policy_lint/selection_git.py +6 -2
- package/scripts/pstack-adapters/common.md +1 -1
- package/scripts/repository_checks/config/constants.py +7 -17
- package/scripts/repository_checks/config/email_exemptions.py +10 -0
- package/scripts/repository_checks/email_exemptions.py +96 -0
- package/scripts/repository_checks/env_var_documentation.py +1 -9
- package/scripts/repository_checks/tracked_secrets.py +10 -4
- package/scripts/test_durable_post_lint.py +42 -9
- package/scripts/tests/test_adapter_configuration.py +8 -0
- package/scripts/tests/test_email_exemptions.py +121 -0
- package/scripts/tests/test_env_var_documentation.py +26 -0
- package/scripts/tests/test_policy_lint_rules_retired_hook_prose.py +197 -0
- package/scripts/tests/test_policy_lint_selection.py +107 -0
- package/scripts/tests/test_tracked_secrets.py +13 -0
- package/hooks/hooks_constants/convergence_branch_constants.py +0 -8
- package/hooks/hooks_constants/destructive_command_environment_constants.py +0 -18
- package/hooks/hooks_constants/test_text_stripping.py +0 -39
- package/hooks/hooks_constants/text_stripping.py +0 -36
|
@@ -18,9 +18,7 @@ from hooks_constants.pre_tool_use_dispatcher_constants import ( # noqa: F401
|
|
|
18
18
|
BASH_TOOL_NAME: str = "Bash"
|
|
19
19
|
POWERSHELL_TOOL_NAME: str = "PowerShell"
|
|
20
20
|
|
|
21
|
-
ALL_SHELL_TOOL_NAMES: frozenset[str] = frozenset(
|
|
22
|
-
{BASH_TOOL_NAME, POWERSHELL_TOOL_NAME}
|
|
23
|
-
)
|
|
21
|
+
ALL_SHELL_TOOL_NAMES: frozenset[str] = frozenset({BASH_TOOL_NAME, POWERSHELL_TOOL_NAME})
|
|
24
22
|
|
|
25
23
|
ALL_GIT_BINARY_BASENAMES: frozenset[str] = frozenset({"git", "git.exe"})
|
|
26
24
|
GIT_COMMIT_SUBCOMMAND: str = "commit"
|
|
@@ -29,12 +27,8 @@ ALL_VALUE_TAKING_GIT_OPTIONS: frozenset[str] = frozenset(
|
|
|
29
27
|
{GIT_WORKING_DIRECTORY_OPTION, "-c", "--git-dir", "--work-tree", "--namespace"}
|
|
30
28
|
)
|
|
31
29
|
GIT_OPTION_WITH_VALUE_STEP: int = 2
|
|
32
|
-
ALL_SHELL_COMMAND_SEPARATOR_TOKENS: frozenset[str] = frozenset(
|
|
33
|
-
|
|
34
|
-
)
|
|
35
|
-
ALL_SHELL_KEYWORD_TOKENS: frozenset[str] = frozenset(
|
|
36
|
-
{"then", "do", "else", "elif"}
|
|
37
|
-
)
|
|
30
|
+
ALL_SHELL_COMMAND_SEPARATOR_TOKENS: frozenset[str] = frozenset({"&&", "||", ";", "|", "&"})
|
|
31
|
+
ALL_SHELL_KEYWORD_TOKENS: frozenset[str] = frozenset({"then", "do", "else", "elif"})
|
|
38
32
|
ALL_COMMAND_WRAPPER_TOKENS: frozenset[str] = frozenset(
|
|
39
33
|
{
|
|
40
34
|
"sudo",
|
|
@@ -85,9 +79,7 @@ SINGLE_DASH_OPTION_PREFIX: str = "-"
|
|
|
85
79
|
DOUBLE_DASH_OPTION_PREFIX: str = "--"
|
|
86
80
|
ALL_SHELL_QUOTE_CHARACTERS: frozenset[str] = frozenset({'"', "'"})
|
|
87
81
|
ALL_COMMAND_BOUNDARY_NEWLINE_CHARACTERS: frozenset[str] = frozenset({"\n", "\r"})
|
|
88
|
-
ENVIRONMENT_ASSIGNMENT_PATTERN: re.Pattern[str] = re.compile(
|
|
89
|
-
r"^[A-Za-z_][A-Za-z0-9_]*="
|
|
90
|
-
)
|
|
82
|
+
ENVIRONMENT_ASSIGNMENT_PATTERN: re.Pattern[str] = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=")
|
|
91
83
|
LINE_CONTINUATION_PATTERN: re.Pattern[str] = re.compile(r"\\\r?\n")
|
|
92
84
|
POWERSHELL_LINE_CONTINUATION_PATTERN: re.Pattern[str] = re.compile(r"`\r?\n")
|
|
93
85
|
|
|
@@ -223,9 +215,7 @@ CATEGORY_HOME_PATH: str = "home-path"
|
|
|
223
215
|
CATEGORY_PRIVATE_IP: str = "private-ip"
|
|
224
216
|
CATEGORY_SECRET: str = "secret"
|
|
225
217
|
|
|
226
|
-
ALL_REDACTED_PREVIEW_CATEGORIES: frozenset[str] = frozenset(
|
|
227
|
-
{CATEGORY_EMAIL, CATEGORY_SECRET}
|
|
228
|
-
)
|
|
218
|
+
ALL_REDACTED_PREVIEW_CATEGORIES: frozenset[str] = frozenset({CATEGORY_EMAIL, CATEGORY_SECRET})
|
|
229
219
|
REDACTED_PREVIEW_PREFIX_LENGTH: int = 4
|
|
230
220
|
REDACTED_PREVIEW_SUFFIX_LENGTH: int = 4
|
|
231
221
|
REDACTED_PREVIEW_REVEALED_LENGTH: int = (
|
|
@@ -238,9 +228,7 @@ MINIMUM_LENGTH_FOR_PARTIAL_REDACTION: int = (
|
|
|
238
228
|
REDACTED_PREVIEW_REVEALED_LENGTH + REDACTED_PREVIEW_MINIMUM_HIDDEN_LENGTH
|
|
239
229
|
)
|
|
240
230
|
|
|
241
|
-
EMAIL_PATTERN: re.Pattern[str] = re.compile(
|
|
242
|
-
r"(?i)\b([A-Z0-9._%+\-]+@[A-Z0-9.\-]+\.[A-Z]{2,})\b"
|
|
243
|
-
)
|
|
231
|
+
EMAIL_PATTERN: re.Pattern[str] = re.compile(r"(?i)\b([A-Z0-9._%+\-]+@[A-Z0-9.\-]+\.[A-Z]{2,})\b")
|
|
244
232
|
|
|
245
233
|
IPV4_PATTERN: re.Pattern[str] = re.compile(
|
|
246
234
|
r"\b((?:25[0-5]|2[0-4]\d|1?\d?\d)\."
|
|
@@ -249,13 +237,9 @@ IPV4_PATTERN: re.Pattern[str] = re.compile(
|
|
|
249
237
|
r"(?:25[0-5]|2[0-4]\d|1?\d?\d))\b"
|
|
250
238
|
)
|
|
251
239
|
|
|
252
|
-
GITHUB_TOKEN_PATTERN: re.Pattern[str] = re.compile(
|
|
253
|
-
r"\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{20,}\b"
|
|
254
|
-
)
|
|
240
|
+
GITHUB_TOKEN_PATTERN: re.Pattern[str] = re.compile(r"\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{20,}\b")
|
|
255
241
|
|
|
256
|
-
GITHUB_FINE_GRAINED_TOKEN_PATTERN: re.Pattern[str] = re.compile(
|
|
257
|
-
r"\bgithub_pat_[A-Za-z0-9_]{20,}\b"
|
|
258
|
-
)
|
|
242
|
+
GITHUB_FINE_GRAINED_TOKEN_PATTERN: re.Pattern[str] = re.compile(r"\bgithub_pat_[A-Za-z0-9_]{20,}\b")
|
|
259
243
|
|
|
260
244
|
AWS_ACCESS_KEY_PATTERN: re.Pattern[str] = re.compile(r"\bAKIA[0-9A-Z]{16}\b")
|
|
261
245
|
|
|
@@ -264,8 +248,9 @@ PEM_PRIVATE_KEY_HEADER_PATTERN: re.Pattern[str] = re.compile(
|
|
|
264
248
|
)
|
|
265
249
|
|
|
266
250
|
HOME_PATH_PATTERN: re.Pattern[str] = HARDCODED_USER_PATH_PATTERN
|
|
251
|
+
INTERPOLATED_HOME_USERNAME_PATTERN: re.Pattern[str] = re.compile(r"^\{[A-Za-z_][A-Za-z0-9_]*\}$")
|
|
267
252
|
|
|
268
|
-
|
|
253
|
+
HOME_USERNAME_MARKER_PATTERN: re.Pattern[str] = re.compile(r"[\\/]+(?:users|home)[\\/]+", re.I)
|
|
269
254
|
|
|
270
255
|
ALL_SECRET_PATTERNS: tuple[re.Pattern[str], ...] = (
|
|
271
256
|
GITHUB_TOKEN_PATTERN,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Shared stdout emitter for PostToolUse hooks that add context."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from hooks_constants.bash_post_call_dispatcher_constants import (
|
|
9
|
+
ADDITIONAL_CONTEXT_KEY,
|
|
10
|
+
HOOK_EVENT_NAME_KEY,
|
|
11
|
+
HOOK_SPECIFIC_OUTPUT_KEY,
|
|
12
|
+
POST_TOOL_USE_HOOK_EVENT_NAME,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def write_post_tool_use_context_to_stdout(context_text: str) -> None:
|
|
17
|
+
"""Write one PostToolUse additionalContext payload to stdout.
|
|
18
|
+
|
|
19
|
+
A hosted hook writes its note here and the Bash PostToolUse dispatcher
|
|
20
|
+
reads it back, joins it with the other hooks' notes, and writes the joined
|
|
21
|
+
text here in turn. Both ends of that hop share this one payload shape
|
|
22
|
+
rather than each carrying a copy of it.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
context_text: The advisory text the dispatcher forwards.
|
|
26
|
+
"""
|
|
27
|
+
payload = {
|
|
28
|
+
HOOK_SPECIFIC_OUTPUT_KEY: {
|
|
29
|
+
HOOK_EVENT_NAME_KEY: POST_TOOL_USE_HOOK_EVENT_NAME,
|
|
30
|
+
ADDITIONAL_CONTEXT_KEY: context_text,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
sys.stdout.write(json.dumps(payload))
|
|
34
|
+
sys.stdout.flush()
|
|
@@ -19,7 +19,6 @@ __all__ = [
|
|
|
19
19
|
"ALL_GH_PR_VIEW_ARGUMENTS",
|
|
20
20
|
"GH_PR_VIEW_TIMEOUT_SECONDS",
|
|
21
21
|
"NO_PULL_REQUEST_MARKER",
|
|
22
|
-
"EXIT_CODE_ERROR_PREFIX",
|
|
23
22
|
"DONE_LABEL_NAME",
|
|
24
23
|
"MERGEABLE_CLEAN_VALUE",
|
|
25
24
|
"MERGEABLE_CONFLICTING_VALUE",
|
|
@@ -59,7 +58,6 @@ ALL_GH_PR_VIEW_ARGUMENTS: tuple[str, ...] = (
|
|
|
59
58
|
)
|
|
60
59
|
GH_PR_VIEW_TIMEOUT_SECONDS: int = 20
|
|
61
60
|
NO_PULL_REQUEST_MARKER: str = "no pull requests found"
|
|
62
|
-
EXIT_CODE_ERROR_PREFIX: str = "Error: Exit code "
|
|
63
61
|
|
|
64
62
|
DONE_LABEL_NAME: str = "done"
|
|
65
63
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Shared stdout emitter for PreToolUse hooks that allow a rewritten tool input."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from hooks_constants.bash_pre_tool_use_dispatcher_constants import (
|
|
9
|
+
ALLOW_DECISION,
|
|
10
|
+
HOOK_EVENT_NAME,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"HOOK_SPECIFIC_OUTPUT_KEY",
|
|
15
|
+
"HOOK_EVENT_NAME_KEY",
|
|
16
|
+
"PERMISSION_DECISION_KEY",
|
|
17
|
+
"UPDATED_INPUT_KEY",
|
|
18
|
+
"write_pre_tool_use_allow_to_stdout",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
HOOK_SPECIFIC_OUTPUT_KEY: str = "hookSpecificOutput"
|
|
22
|
+
HOOK_EVENT_NAME_KEY: str = "hookEventName"
|
|
23
|
+
PERMISSION_DECISION_KEY: str = "permissionDecision"
|
|
24
|
+
UPDATED_INPUT_KEY: str = "updatedInput"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def write_pre_tool_use_allow_to_stdout(
|
|
28
|
+
all_updated_tool_input_fields: dict[str, object],
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Write one PreToolUse allow payload carrying a rewritten tool input.
|
|
31
|
+
|
|
32
|
+
A rewriter writes its replacement input here and the Bash PreToolUse
|
|
33
|
+
dispatcher reads it back, carrying the updatedInput through to the harness.
|
|
34
|
+
Both ends of that hop share this one payload shape rather than each holding
|
|
35
|
+
a copy of it.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
all_updated_tool_input_fields: The full tool_input the tool call should run with.
|
|
39
|
+
"""
|
|
40
|
+
payload = {
|
|
41
|
+
HOOK_SPECIFIC_OUTPUT_KEY: {
|
|
42
|
+
HOOK_EVENT_NAME_KEY: HOOK_EVENT_NAME,
|
|
43
|
+
PERMISSION_DECISION_KEY: ALLOW_DECISION,
|
|
44
|
+
UPDATED_INPUT_KEY: all_updated_tool_input_fields,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
sys.stdout.write(json.dumps(payload))
|
|
48
|
+
sys.stdout.flush()
|
|
@@ -7,22 +7,29 @@ from hooks_constants.bash_post_call_dispatcher_constants import (
|
|
|
7
7
|
from hooks_constants.bash_pre_tool_use_dispatcher_constants import BASH_TOOL_NAME
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
def
|
|
10
|
+
def test_roster_runs_the_recorder_then_the_pr_reminder_then_the_msys_advisor() -> None:
|
|
11
11
|
all_script_paths = [
|
|
12
12
|
each_entry.script_relative_path for each_entry in ALL_BASH_POST_TOOL_USE_HOSTED_HOOK_ENTRIES
|
|
13
13
|
]
|
|
14
14
|
assert all_script_paths == [
|
|
15
15
|
"observability/test_failure_recorder.py",
|
|
16
16
|
"advisory/pr_done_reminder.py",
|
|
17
|
+
"advisory/msys_path_conversion_advisor.py",
|
|
17
18
|
]
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
def test_pr_done_reminder_also_serves_the_powershell_tool() -> None:
|
|
21
|
-
reminder_entry = ALL_BASH_POST_TOOL_USE_HOSTED_HOOK_ENTRIES[
|
|
22
|
+
reminder_entry = ALL_BASH_POST_TOOL_USE_HOSTED_HOOK_ENTRIES[1]
|
|
22
23
|
assert reminder_entry.script_relative_path == "advisory/pr_done_reminder.py"
|
|
23
24
|
assert "PowerShell" in reminder_entry.applicable_tool_names
|
|
24
25
|
|
|
25
26
|
|
|
27
|
+
def test_msys_advisor_serves_the_bash_tool_only() -> None:
|
|
28
|
+
advisor_entry = ALL_BASH_POST_TOOL_USE_HOSTED_HOOK_ENTRIES[-1]
|
|
29
|
+
assert advisor_entry.script_relative_path == "advisory/msys_path_conversion_advisor.py"
|
|
30
|
+
assert advisor_entry.applicable_tool_names == frozenset({"Bash"})
|
|
31
|
+
|
|
32
|
+
|
|
26
33
|
def test_context_forwarding_keys_match_the_hook_output_contract() -> None:
|
|
27
34
|
assert constants.POST_TOOL_USE_HOOK_EVENT_NAME == "PostToolUse"
|
|
28
35
|
assert constants.HOOK_SPECIFIC_OUTPUT_KEY == "hookSpecificOutput"
|
|
@@ -7,7 +7,14 @@ ALL_CONSTANT_BINDINGS = run_path(
|
|
|
7
7
|
str(Path(__file__).with_name("bash_pre_tool_use_dispatcher_constants.py"))
|
|
8
8
|
)
|
|
9
9
|
ALL_BASH_HOSTED_HOOK_ENTRIES = ALL_CONSTANT_BINDINGS["ALL_BASH_HOSTED_HOOK_ENTRIES"]
|
|
10
|
+
ALL_BASH_ONLY_TOOL_NAMES = ALL_CONSTANT_BINDINGS["ALL_BASH_ONLY_TOOL_NAMES"]
|
|
11
|
+
BashHostedHookEntry = ALL_CONSTANT_BINDINGS["BashHostedHookEntry"]
|
|
10
12
|
|
|
11
13
|
|
|
12
|
-
def
|
|
13
|
-
assert ALL_BASH_HOSTED_HOOK_ENTRIES == (
|
|
14
|
+
def test_roster_hosts_only_the_msys_rewriter_and_no_blocking_hook() -> None:
|
|
15
|
+
assert ALL_BASH_HOSTED_HOOK_ENTRIES == (
|
|
16
|
+
BashHostedHookEntry(
|
|
17
|
+
script_relative_path="blocking/msys_rev_path_rewriter.py",
|
|
18
|
+
applicable_tool_names=ALL_BASH_ONLY_TOOL_NAMES,
|
|
19
|
+
),
|
|
20
|
+
)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Constants for the failing-test-run PostToolUse recorder hook.
|
|
2
2
|
|
|
3
3
|
Holds the cheap substring check the hook runs before any shell parsing, the
|
|
4
|
-
|
|
4
|
+
chaining and redirection operator
|
|
5
5
|
tokens that keep the recognizer to one unchained command, the pytest program
|
|
6
6
|
and Python interpreter basenames, and the flag-token marker the path
|
|
7
7
|
extractor uses to skip an option rather than an existing test file argument.
|
|
@@ -20,7 +20,6 @@ import re
|
|
|
20
20
|
BASH_TOOL_NAME = "Bash"
|
|
21
21
|
|
|
22
22
|
ALL_PYTEST_MENTION_SUBSTRINGS: tuple[str, ...] = ("pytest", "py.test")
|
|
23
|
-
EXIT_CODE_ERROR_PREFIX: str = "Error: Exit code "
|
|
24
23
|
OPTION_TOKEN_PREFIX: str = "-"
|
|
25
24
|
NODE_ID_FILE_PATH_SEPARATOR: str = "::"
|
|
26
25
|
ALL_CHAINING_OPERATOR_SUBSTRINGS: tuple[str, ...] = ("&&", "||", ";", "|", "\n")
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Tests for the shared PostToolUse context emitter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from hooks_constants.post_tool_use_context import write_post_tool_use_context_to_stdout
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_should_write_the_post_tool_use_payload_carrying_the_context_text(
|
|
13
|
+
capsys: pytest.CaptureFixture[str],
|
|
14
|
+
) -> None:
|
|
15
|
+
write_post_tool_use_context_to_stdout("=== PR DONE CHECKLIST ===")
|
|
16
|
+
|
|
17
|
+
assert json.loads(capsys.readouterr().out) == {
|
|
18
|
+
"hookSpecificOutput": {
|
|
19
|
+
"hookEventName": "PostToolUse",
|
|
20
|
+
"additionalContext": "=== PR DONE CHECKLIST ===",
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Tests for the shared PreToolUse allow-with-rewrite stdout emitter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from io import StringIO
|
|
7
|
+
from unittest.mock import patch
|
|
8
|
+
|
|
9
|
+
from hooks_constants.pre_tool_use_allow_output import write_pre_tool_use_allow_to_stdout
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _emitted_payload(updated_tool_input: dict[str, object]) -> dict[str, object]:
|
|
13
|
+
"""Return the parsed payload the emitter wrote for one updated tool input."""
|
|
14
|
+
captured_stdout = StringIO()
|
|
15
|
+
with patch("sys.stdout", captured_stdout):
|
|
16
|
+
write_pre_tool_use_allow_to_stdout(updated_tool_input)
|
|
17
|
+
return json.loads(captured_stdout.getvalue())
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_emitter_writes_an_allow_decision_carrying_the_updated_input() -> None:
|
|
21
|
+
assert _emitted_payload({"command": "git status", "description": "check"}) == {
|
|
22
|
+
"hookSpecificOutput": {
|
|
23
|
+
"hookEventName": "PreToolUse",
|
|
24
|
+
"permissionDecision": "allow",
|
|
25
|
+
"updatedInput": {"command": "git status", "description": "check"},
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -101,6 +101,9 @@ try:
|
|
|
101
101
|
for each_bootstrap_directory in (_hooks_root_directory, _blocking_directory):
|
|
102
102
|
if each_bootstrap_directory not in sys.path:
|
|
103
103
|
sys.path.insert(0, each_bootstrap_directory)
|
|
104
|
+
from hooks_constants.bash_post_call_dispatcher_constants import (
|
|
105
|
+
EXIT_CODE_ERROR_PREFIX,
|
|
106
|
+
)
|
|
104
107
|
from hooks_constants.pre_tool_use_stdin import read_hook_input_dictionary_from_stdin
|
|
105
108
|
from hooks_constants.test_failure_recorder_constants import (
|
|
106
109
|
ALL_CHAINING_OPERATOR_SUBSTRINGS,
|
|
@@ -108,7 +111,6 @@ try:
|
|
|
108
111
|
ALL_PYTEST_PROGRAM_BASENAMES,
|
|
109
112
|
ALL_REDIRECTION_TOKENS,
|
|
110
113
|
BASH_TOOL_NAME,
|
|
111
|
-
EXIT_CODE_ERROR_PREFIX,
|
|
112
114
|
MODULE_RUN_FLAG,
|
|
113
115
|
NODE_ID_FILE_PATH_SEPARATOR,
|
|
114
116
|
OPTION_TOKEN_PREFIX,
|
package/package.json
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
Route every user-directed question through the `AskUserQuestion` tool — never a plain-text question in a response's final paragraph. Structure: concise `question`, `header` of 12 chars or fewer, 2-4 options (the UI adds the "Other" fallback), `multiSelect` only when choices genuinely combine.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
No hook checks this. Read your own final paragraph before you send it. A paragraph that ends in a question mark, or that carries an ask-phrase such as "would you like", "should I", or "let me know if", belongs in an `AskUserQuestion` call instead. A rhetorical question answered in the same paragraph is fine, and so is a question inside code or a blockquote. `verify-before-asking` settles whether the question belongs to the user at all.
|
|
6
6
|
|
|
7
7
|
## The question block stays lean
|
|
8
8
|
|
|
9
9
|
`AskUserQuestion` renders as one plain unformatted text block. Detail — plans, counts, tradeoffs, background — goes in chat text before the call. The block itself carries a lean question and short choices. When a choice needs formatting, an inline visualizer tool carries it.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
No hook denies an over-full block, so keep an `AskUserQuestion` call inside these caps yourself. They apply to the `question` text and to the `description` under any of the `options`:
|
|
12
12
|
|
|
13
13
|
| What the block carries | Cap |
|
|
14
14
|
|---|---|
|
|
@@ -28,4 +28,4 @@ Structure is read at block level on the raw text: a marker counts when it opens
|
|
|
28
28
|
|
|
29
29
|
An inline code span — a path, a flag, a command the reader needs verbatim — weighs one word against either word cap on both fields, so a question naming `--dry-run` and a choice naming `C:\dev\gate.py` both pass. A span sits inside a line, so it never opens one with a marker.
|
|
30
30
|
|
|
31
|
-
A sentence closes on `.`, `!`, or `?` followed by a capitalized word or by the end of the text. A word is any whitespace-separated token carrying a letter or a digit.
|
|
31
|
+
A sentence closes on `.`, `!`, or `?` followed by a capitalized word or by the end of the text. A word is any whitespace-separated token carrying a letter or a digit. When a block breaks a cap, move that detail back into chat text before the call.
|
package/rules/code-standards.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> **Canonical review contract:** [`CODE_RULES.md`](../docs/CODE_RULES.md) — the human and AI review contract for code quality, loaded on demand.
|
|
4
4
|
> **Checked-in pointer:** [`.cursor/BUGBOT.md`](../../../.cursor/BUGBOT.md) — the file Cursor BugBot reads; it points at `CODE_RULES.md`.
|
|
5
|
-
> **Production enforcement
|
|
5
|
+
> **Production enforcement.** The staged policy lint runs `hooks/blocking/code_rules_enforcer.py` over each changed file. No write-time hook runs it. Use `python packages/claude-dev-env/scripts/cde_lint.py --staged` before you commit, and CI runs the same lint against the merge base. Each mechanical rule carries a synchronization test.
|
|
6
6
|
|
|
7
7
|
## Policy surface map
|
|
8
8
|
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
|---|---|---|
|
|
11
11
|
| Contract | `docs/CODE_RULES.md` | Full review criteria for PR agents, loaded on demand |
|
|
12
12
|
| Pointer | `.cursor/BUGBOT.md` | Checked-in file Cursor BugBot reads; points at `CODE_RULES.md` |
|
|
13
|
-
| Enforcer | `hooks/blocking/code_rules_enforcer.py` | Hand-maintained
|
|
13
|
+
| Enforcer | `hooks/blocking/code_rules_enforcer.py` | Hand-maintained checks the staged policy lint runs; not generated from the docs |
|
|
14
|
+
| Lint | `scripts/cde_lint.py` | Runs the enforcer and the other policy rules over staged or changed files |
|
|
14
15
|
| Session rules | `rules/*.md` | Runtime session policy (questions, tasks, shell) |
|
|
15
16
|
|
|
16
17
|
Load `CODE_RULES.md` when reviewing a PR, resolving a policy conflict, or generating code. Prefer linking this ref over restating rules.
|
|
@@ -6,4 +6,6 @@ How to ask: plain language, one option per path with its tradeoff in the descrip
|
|
|
6
6
|
|
|
7
7
|
Not a fork (just proceed): trivially reversible internal-only choices, anything the codebase / stated goal / an existing rule already determines (see `verify-before-asking`), or a single viable path.
|
|
8
8
|
|
|
9
|
+
Naming a gap in chat is not surfacing a fork. Saying "no production path calls this yet" or "that is a follow-up" describes the fork; it does not ask it. Shipping code that nothing calls is a fork, because the alternative — wire the caller, or hold the change — changes the deliverable. Route it through `AskUserQuestion` before the merge, not into the closing summary after it.
|
|
10
|
+
|
|
9
11
|
When an option you offered names a repair and the answer leaves it undone, write it down before the turn moves on: a todo entry, a `spawn_task` chip, or a filed issue. An answer of "no preference" settles which path you take now. It leaves the repair you found still waiting.
|
|
@@ -1,41 +1,41 @@
|
|
|
1
1
|
# Destructive Commands in Bash
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
No hook watches Bash commands for destructive patterns. What you face is the harness permission prompt. The harness raises it from the session permission mode and the permission rules in effect on your host. In a background or auto-mode run no human can answer that prompt, so the call stalls.
|
|
4
4
|
|
|
5
|
-
Two consequences follow
|
|
5
|
+
Two consequences follow. Use an allowed removal form, and keep a destructive literal out of the command string even when it rides only as data.
|
|
6
6
|
|
|
7
|
-
## Removal forms
|
|
7
|
+
## Removal forms to prefer
|
|
8
8
|
|
|
9
|
-
- **Scratch and probe files
|
|
10
|
-
- **Worktrees
|
|
11
|
-
- **Tracked files
|
|
12
|
-
- **Bash `rm` when unavoidable
|
|
9
|
+
- **Scratch and probe files.** Use the PowerShell tool with `Remove-Item -Recurse -Force -Confirm:$false <absolute path>`.
|
|
10
|
+
- **Worktrees.** Use `git worktree remove --force <path>`.
|
|
11
|
+
- **Tracked files.** Use `git rm <path>`, which records the deletion in the index.
|
|
12
|
+
- **Bash `rm` when unavoidable.** Write one standalone `rm` with absolute literal paths, no chaining, and no globs. Keep every target inside the ephemeral namespace below.
|
|
13
13
|
|
|
14
|
-
## The ephemeral namespace
|
|
14
|
+
## The ephemeral namespace
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Keep a Bash `rm` to targets that resolve inside one of these:
|
|
17
17
|
|
|
18
|
-
- The OS temporary root
|
|
18
|
+
- The OS temporary root.
|
|
19
19
|
- A path rooted at `/tmp` or `/temp`, drive-letter tolerant.
|
|
20
20
|
- A path holding a `/worktrees/` or `/worktree/` segment, or a directory git reports inside a worktree admin directory.
|
|
21
21
|
- `~/.claude`.
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Never pass a bare ephemeral root, such as `/tmp`, the OS temp root itself, or a bare directory named `worktrees` or `worktree`. A single stray argument then wipes the whole namespace.
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
Write each target as a literal path. A variable, a `$(...)` or backtick expansion, or a brace glob hides what the command will delete from the reader and from the permission matcher.
|
|
26
26
|
|
|
27
|
-
A file left in the OS temp directory or under `$CLAUDE_JOB_DIR` is cleaned by the harness and needs no explicit removal
|
|
27
|
+
A file left in the OS temp directory or under `$CLAUDE_JOB_DIR` is cleaned by the harness and needs no explicit removal. See the exception clause in [`cleanup-temp-files.md`](cleanup-temp-files.md).
|
|
28
28
|
|
|
29
29
|
## Keep destructive literals out of the command string
|
|
30
30
|
|
|
31
|
-
A destructive literal carried only as data
|
|
31
|
+
The permission matcher reads the raw command string. A destructive literal carried only as data still sits in that string, so it can push the command out of an allowed shape and into a prompt even though the shell never executes it. This covers a commit message, a PR or issue body, an echoed string, a `python -c` or `node -e` or `awk` argument, and a heredoc.
|
|
32
32
|
|
|
33
|
-
- Bodies that describe destructive-command behavior go in a file passed by path
|
|
34
|
-
- To exercise or verify
|
|
33
|
+
- Bodies that describe destructive-command behavior go in a file passed by path, such as `git commit -F <file>` or `gh … --body-file <file>`. See [`gh-cli-conventions.md`](gh-cli-conventions.md). Never `git commit -m` or `gh … -b`.
|
|
34
|
+
- To exercise or verify a hook, run the committed test suite with `python -m pytest <test_file>`, which passes the command strings as in-language data. Never an inline `python -c` harness.
|
|
35
35
|
|
|
36
36
|
## Every subagent prompt carries the rule
|
|
37
37
|
|
|
38
|
-
A prompt-delivered directive reaches only the agent that gets it. An agent that spawns its own workers
|
|
38
|
+
A prompt-delivered directive reaches only the agent that gets it. An agent that spawns its own workers, such as review lenses, fix agents, or verifiers, copies this line into every subagent prompt it issues. A grandchild cleaning up its own probe file then uses an allowed form:
|
|
39
39
|
|
|
40
40
|
> Never use bash rm in any form. Delete scratch/probe files with the PowerShell tool (Remove-Item -Recurse -Force -Confirm:$false <absolute path>), or leave them in the OS temp dir; remove worktrees only via git worktree remove --force.
|
|
41
41
|
|
|
@@ -43,5 +43,5 @@ Prefer that a child leaves its scratch files for the parent to remove at teardow
|
|
|
43
43
|
|
|
44
44
|
## Sibling rules
|
|
45
45
|
|
|
46
|
-
- [`cleanup-temp-files.md`](cleanup-temp-files.md)
|
|
47
|
-
- [`windows-filesystem-safe.md`](windows-filesystem-safe.md)
|
|
46
|
+
- [`cleanup-temp-files.md`](cleanup-temp-files.md) names which scratch files a task removes, and which it leaves.
|
|
47
|
+
- [`windows-filesystem-safe.md`](windows-filesystem-safe.md) holds the safe `rmtree` and `force_rmtree` patterns for read-only Windows files.
|
|
@@ -11,13 +11,13 @@ paths:
|
|
|
11
11
|
|
|
12
12
|
# Documentation Inventory Integrity
|
|
13
13
|
|
|
14
|
-
A doc that inventories code is a contract: a reader trusts the listing to map the directory, trusts a shown command to run, and trusts a table row to name the file that reads the variable. Three
|
|
14
|
+
A doc that inventories code is a contract: a reader trusts the listing to map the directory, trusts a shown command to run, and trusts a table row to name the file that reads the variable. Three repository checks hold the three inventory shapes in step with the code. No write-time hook runs them. Run `python packages/claude-dev-env/scripts/repository_policy.py` before you commit, and CI runs the same command.
|
|
15
15
|
|
|
16
16
|
## 1. A per-directory `CLAUDE.md` names files that exist
|
|
17
17
|
|
|
18
18
|
Every bare filename a per-directory `CLAUDE.md` names points at a file in the subtree that `CLAUDE.md` describes — both the filenames its table cells list and the scripts its fenced run commands invoke (`python script.py`). Add the row and the run command in the change that adds the file; drop both in the change that removes it.
|
|
19
19
|
|
|
20
|
-
`
|
|
20
|
+
`repository_checks/claude_md.py` scans every tracked `CLAUDE.md` in the committed tree and reports each orphan it holds, loading its detection logic from `claude_md_orphan_file_blocker.py`. It reads committed files rather than a pending write, so it reports a pre-existing orphan alongside a new one.
|
|
21
21
|
|
|
22
22
|
It collects two kinds of reference:
|
|
23
23
|
|
|
@@ -32,7 +32,7 @@ The check stays quiet for a target that is not a `CLAUDE.md`, for a cell holding
|
|
|
32
32
|
|
|
33
33
|
A package directory that documents its own files in a `README.md` Layout table, a `CLAUDE.md` "Key files" list, or a skill `SKILL.md` Layout table keeps that inventory in step with the directory. A new production file in such a directory gets its entry — a table row or a list bullet naming the file in backticks and saying what it does — in the same change.
|
|
34
34
|
|
|
35
|
-
`
|
|
35
|
+
`repository_checks/package_inventory.py` scans the committed tree and reports a production file whose basename appears in no present inventory, loading its detection logic from `package_inventory_stale_blocker.py`. It names the fix. A skill `SKILL.md` Layout table that maps `scripts/` counts as the inventory for files in that subdirectory.
|
|
36
36
|
|
|
37
37
|
Two free-prose slices stay with judgment and belong in the same change:
|
|
38
38
|
|
|
@@ -45,4 +45,4 @@ This is the `category-o-docstring-vs-impl-drift` (O8) orphaned-doc-claim shape a
|
|
|
45
45
|
|
|
46
46
|
Every row in an env-var summary table pairs an UPPER_SNAKE variable with a code-file path that reads it — written as `` | `GOOGLE_APPLICATION_CREDENTIALS` | `auth/google_auth.py` | … | ``. When a code change removes the last read of a variable from a file, the same change drops or corrects the row naming that file.
|
|
47
47
|
|
|
48
|
-
`
|
|
48
|
+
`repository_checks/env_var_documentation.py` scans tracked `.md` files and reports a row whose named code file exists yet never references the variable, loading its detection logic from `env_var_table_code_drift_blocker.py`. It names the fix. A row whose code file resolves nowhere stays quiet, since the check cannot prove the drift.
|
|
@@ -28,13 +28,13 @@ Read the body and the docstring side by side. Apply each check that matches the
|
|
|
28
28
|
- **Gate-outcome status flags** — an outcome routed to a blocker (`blocker = ...; break`) reads as blocked everywhere, never as a bypass.
|
|
29
29
|
- **Returns / Raises / Note claims** — each free-form claim matches the body.
|
|
30
30
|
|
|
31
|
-
Many deterministic shapes of this drift
|
|
31
|
+
Many deterministic shapes of this drift are checked in `code_rules_docstrings.py` (and the JS and `.mjs` slices in `code_rules_imports_logging.py`). The staged policy lint reaches both through `code_rules_enforcer.py`, and no write-time hook runs them. Run `python packages/claude-dev-env/scripts/cde_lint.py --staged` before you commit. Free-form rest is judgment.
|
|
32
32
|
|
|
33
33
|
## Hook prose matches its detector
|
|
34
34
|
|
|
35
35
|
A hook module is the sharpest case of the same rule: its docstring lead narrative and its `CORRECTIVE_MESSAGE` describe exactly the shapes the detector flags, and claim no broader trigger surface than the regex enforces.
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
The staged policy lint carries this as its `hook-prose-consistency` rule, covering hook modules and their `*_constants.py` companions. It reports prose that claims a trigger the detector never fires on, and names the fix. No write-time hook runs it, so run `python packages/claude-dev-env/scripts/cde_lint.py --staged` before you commit.
|
|
38
38
|
|
|
39
39
|
After writing a hook, ask: would a token matching every word of this message actually trip the detector? When the message names a shape the regex skips, rewrite the message to name only what the regex catches. The path-shape case is the common overstatement — a detector that keys off a path separator must not claim it blocks an "output-key segment". The corrective message spells the rewrite.
|
|
40
40
|
|
|
@@ -64,9 +64,11 @@ The report then presents each candidate to the owner for a durable-fix decision,
|
|
|
64
64
|
|
|
65
65
|
## Enforcement
|
|
66
66
|
|
|
67
|
-
`code_rules_blast_radius.py`
|
|
67
|
+
`code_rules_blast_radius.py` runs inside `code_rules_enforcer.py`, which the staged policy lint applies to each changed file under its `code-rules` rule. No write-time hook runs it, so run `python packages/claude-dev-env/scripts/cde_lint.py --staged` before you commit. CI runs the same lint against the merge base.
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
The check requires each raised type written directly inside a loop body to end in `RunFatal` or `ItemBlocked`. The lexical check covers raises written directly in loop bodies. Shared helpers carry multiple caller contexts, so their callers classify the boundary.
|
|
70
|
+
|
|
71
|
+
Findings compare each changed file against its baseline. A raise already present in the baseline stays accepted, so the check reports only newly written raises.
|
|
70
72
|
|
|
71
73
|
## Excerpt for repository-instruction sessions
|
|
72
74
|
|
|
@@ -47,5 +47,5 @@ Issue one shell search at a time when the walk is large. Parallel full-tree sear
|
|
|
47
47
|
|
|
48
48
|
## Search controls
|
|
49
49
|
|
|
50
|
-
- `unscoped_search_blocker`
|
|
50
|
+
- No hook denies a walk from an unscoped root. `unscoped_search_blocker` went with the blocking policy hooks in `0f21faf8e`, which left the Bash PreToolUse roster empty. The scope invariant above is guidance a reader follows.
|
|
51
51
|
- For Everything searches that use a project name, run `python "${CLAUDE_SKILL_DIR}/scripts/everything_search.py" <project-name> <search arguments>`. An exact project name becomes its path from `~/.claude/project-paths.json`. The command then starts `es.exe` without a shell. `scripts/setup_project_paths.py` writes the registry.
|
package/rules/git-workflow.md
CHANGED
|
@@ -50,6 +50,40 @@ untouched.
|
|
|
50
50
|
- Before push: `gh pr ready --undo`
|
|
51
51
|
- After review approved: `gh pr ready`
|
|
52
52
|
|
|
53
|
+
## Run the required checks locally before the first push
|
|
54
|
+
|
|
55
|
+
Read the branch ruleset for the required check contexts before you push a branch,
|
|
56
|
+
or any level of a stack: `gh api repos/<owner>/<repo>/rules/branches/<trunk>`.
|
|
57
|
+
Then run that exact gate command locally against that level's own base, on every
|
|
58
|
+
level. A required check that never fired is invisible debt at every level, and it
|
|
59
|
+
surfaces only after the whole stack is pushed, when the repair costs a second pass
|
|
60
|
+
over every branch.
|
|
61
|
+
|
|
62
|
+
A checks listing that reports nothing on the branch is a finding, not a neutral
|
|
63
|
+
state. Find out whether the workflow's event filters exclude the branch, or whether
|
|
64
|
+
the check simply never ran, before you treat that branch as clean.
|
|
65
|
+
|
|
66
|
+
## Each stack level stands on its own
|
|
67
|
+
|
|
68
|
+
A symbol belongs at the level that first **uses** it, not the level that first
|
|
69
|
+
mentions it. A bottom pull request that declares the imports its descendants will
|
|
70
|
+
need fails the linter on unused imports. A test helper that calls a function three
|
|
71
|
+
levels above it fails on an undefined name. Both defects stay invisible while you
|
|
72
|
+
read the finished tip, and both are obvious the moment you check one level alone.
|
|
73
|
+
|
|
74
|
+
Prove each level before you push it: import the modules that level changes, and run
|
|
75
|
+
the required linter against that level's own base. To repair a level, rebuild its
|
|
76
|
+
import header as the union of what that level references, let the linter's
|
|
77
|
+
autofix strip the rest, and move a premature helper up to the level that defines
|
|
78
|
+
what it calls.
|
|
79
|
+
|
|
80
|
+
## A force-push that moves content obliges a description refresh
|
|
81
|
+
|
|
82
|
+
Force-with-lease protects the ref. It protects nobody's understanding of what the
|
|
83
|
+
branch now holds. When a rewrite moves content between levels of a stack, or
|
|
84
|
+
otherwise changes what a branch contains, refresh that pull request's description
|
|
85
|
+
through the `pr-description-writer` agent before you ask anyone to read or merge it.
|
|
86
|
+
|
|
53
87
|
## Never commit working documents or images
|
|
54
88
|
|
|
55
89
|
**NEVER commit these files to the repo:**
|
|
@@ -18,6 +18,8 @@ Before you end any turn, read your last paragraph. If it is a plan, an analysis,
|
|
|
18
18
|
|
|
19
19
|
In an autonomous pipeline the user cannot answer mid-task. For reversible actions that follow from the original request, act without asking; save any follow-up offers for after the task is done.
|
|
20
20
|
|
|
21
|
+
Authority the task already granted stays granted. A later preference about tone, brevity, or reply format changes how you report, never what you are allowed to do. Re-asking for permission the task already gave hands the work back to the user, who then does it by hand.
|
|
22
|
+
|
|
21
23
|
## Delegate and keep working
|
|
22
24
|
|
|
23
25
|
Hand independent subtasks to subagents and keep working while they run; let them run in the background rather than block until each one returns. Reuse a long-lived subagent across related subtasks so its context carries forward and saves repeated reads. Step in when a subagent drifts off track or is missing context.
|
|
@@ -16,9 +16,9 @@ which cannot be answered unattended, or belong to other hosts.
|
|
|
16
16
|
|
|
17
17
|
The runner loads the key with paramiko and signs in the same process. The command-line clients check
|
|
18
18
|
the key file's permissions first and refuse it, and Git Bash's `ssh` then falls back to a password
|
|
19
|
-
prompt that hangs an unattended run.
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
prompt that hangs an unattended run. No hook denies an ssh-family word aimed at the NAS, so keep to
|
|
20
|
+
the runner yourself. When the `System32/OpenSSH` binary is unavoidable, pass `-o BatchMode=yes` so it
|
|
21
|
+
fails fast on a refused key.
|
|
22
22
|
|
|
23
23
|
Host, ssh port and ssh user are constants inside the runner and are also under the `nas` key in
|
|
24
24
|
`~/.claude/local-identity.json`. Keep all three out of anything committed or posted.
|