Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 36 additions & 4 deletions src/context_system/prompt_assembly.py
Original file line number Diff line number Diff line change
Expand Up @@ -1002,12 +1002,44 @@ def _emit_group(
"understand and review your work. This is CRITICAL.\n"
" - To read files use Read instead of cat, head, tail, or sed\n"
" - To edit files use Edit instead of sed or awk\n"
" - To create files use Write instead of cat with heredoc or echo\n"
# The scope clause is the port's own rationale ("allows the user to
# better understand and review your work") applied consistently: it is
# about files the user reviews. Unscoped, the rule also caught scratch
# scripts, which cost a step every time — clawcodex authors a probe
# script with Write and then spends a second step running it, where the
# current Claude Code does both in one Bash call. Measured on seven
# shared terminal-bench 2.1 tasks at the same model and effort
# (2026-07-26): Claude Code authors 23.4% of its Bash calls via heredoc
# against clawcodex's 2.7%, and spends 13.6% of its tool calls on
# Write/Edit against clawcodex's 25.5%. The reference's own wording is a
# single soft line ("Prefer the dedicated file/search tools over shell
# commands when one fits"), so the absolute reading is not faithful either.
# Location is deliberately unspecified: it makes no permission difference.
# check_accept_edits_bash (src/permissions/bash_mode_validation.py:304)
# gates on _has_shell_redirection, not on where the redirect points --
# `cat` is in ACCEPT_EDITS_READ_ONLY_COMMANDS and that branch requires
# `not redirection_anywhere` -- so every authoring form returns False and
# prompts, in-roots and /tmp alike (verified for `cat > /app/x <<PY`,
# `cat > /tmp/x <<PY`, `cat > ./x <<PY`, `printf 'x' > /app/x`).
#
# KNOWN COST, accepted deliberately: in acceptEdits mode Write is
# auto-accepted while a Bash heredoc prompts, so taking this option trades
# a saved step for an approval prompt there. It is a clear win only where
# Bash is already unprompted (headless, bypass). Hence "may" rather than
# an instruction -- the model still has Write available and should use it
# when a prompt would cost more than the step saves.
" - To create files use Write instead of cat with heredoc or echo. A "
"throwaway script you run once and discard is part of the command "
"rather than a deliverable, and may instead be authored inline in the "
"same Bash call that runs it.\n"
" - To search for files use Glob instead of find or ls\n"
" - To search the content of files, use Grep instead of grep or rg\n"
" - Reserve using the Bash exclusively for system commands and terminal "
"operations that require shell execution. If you are unsure and there "
"is a relevant dedicated tool, default to using the dedicated tool.\n"
# "exclusively" was dropped: it cannot coexist with the throwaway-script
# carve-out above, and a prompt that states a rule and its exception in
# absolute terms leaves the model to guess which one governs.
" - Reserve Bash for system commands and terminal operations that "
"require shell execution. If you are unsure and there is a relevant "
"dedicated tool, default to using the dedicated tool.\n"
"- Break down and manage your work with the TaskCreate tool.\n"
# Two clauses were dropped in the port, and together they are what turns
# a permission into a practice: the imperative to maximize, and the
Expand Down
32 changes: 32 additions & 0 deletions tests/test_system_prompt_full.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,38 @@ def test_tools_prompt_pushes_parallel_tool_calls(self):
"the dependency carve-out is what makes parallelizing safe to act on"
)

def test_throwaway_scripts_may_be_authored_inline(self):
"""A one-shot script is part of the command, not a deliverable.

The Write preference must survive for deliverables — that is what
keeps real edits reviewable as diffs — while a throwaway script the
agent runs once may be authored inside the Bash call that runs it,
saving a step.

Stated as permission ("may"), not instruction, because the tradeoff
is context-dependent: in acceptEdits mode Write is auto-accepted
while any Bash redirect prompts (check_accept_edits_bash gates on
_has_shell_redirection, so the target path is irrelevant), so the
saved step costs an approval there.

No location is specified — /tmp and in-roots paths prompt
identically, so mandating one buys nothing.
"""
prompt = build_full_system_prompt(use_cache=False)
assert "To create files use Write instead of cat with heredoc" in prompt, (
"the deliverable path must still prefer Write, for reviewable diffs"
)
assert "throwaway script you run once and discard" in prompt
assert "may instead be authored inline" in prompt, (
"permission, not instruction — the tradeoff depends on whether "
"Bash is already unprompted in the caller's permission mode"
)
tools_section = prompt.split("# Using your tools")[1].split("\n- Break")[0]
assert "/tmp" not in tools_section
assert "exclusively" not in tools_section, (
"an absolute 'Reserve Bash exclusively' contradicts the carve-out"
)

def test_identity_prompt_backward_compat(self):
"""_IDENTITY_PROMPT is an alias for _INTRO_SECTION."""
assert _IDENTITY_PROMPT is _INTRO_SECTION
Expand Down
Loading