From 5ebfc613ed90ce864434e3be80221e738ca47eb3 Mon Sep 17 00:00:00 2001 From: Timor Gruber Date: Sun, 4 Oct 2026 01:33:12 +0300 Subject: [PATCH 1/4] share the strict output style template Keep Claude's Strict style in one reusable template so other agents can consume the same rules without duplicating them. --- .../strict-output-style.md.tmpl | 6 ------ dot_claude/output-styles/private_strict.md.tmpl | 7 +++++++ 2 files changed, 7 insertions(+), 6 deletions(-) rename dot_claude/output-styles/private_strict.md => .chezmoitemplates/strict-output-style.md.tmpl (98%) create mode 100644 dot_claude/output-styles/private_strict.md.tmpl diff --git a/dot_claude/output-styles/private_strict.md b/.chezmoitemplates/strict-output-style.md.tmpl similarity index 98% rename from dot_claude/output-styles/private_strict.md rename to .chezmoitemplates/strict-output-style.md.tmpl index 7269520..fc8d847 100644 --- a/dot_claude/output-styles/private_strict.md +++ b/.chezmoitemplates/strict-output-style.md.tmpl @@ -1,9 +1,3 @@ ---- -name: Strict -description: List-first output, fixed order, code-anchored, no filler — tuned for low visual load -keep-coding-instructions: true ---- - ## The shape of every answer This shape governs conversational replies to the user, and nothing else. Do not diff --git a/dot_claude/output-styles/private_strict.md.tmpl b/dot_claude/output-styles/private_strict.md.tmpl new file mode 100644 index 0000000..bd2892d --- /dev/null +++ b/dot_claude/output-styles/private_strict.md.tmpl @@ -0,0 +1,7 @@ +--- +name: Strict +description: List-first output, fixed order, code-anchored, no filler — tuned for low visual load +keep-coding-instructions: true +--- + +{{ template "strict-output-style.md.tmpl" . -}} From 4dd7a8e8d7c4a0c7b67746f16cbd0222c4491359 Mon Sep 17 00:00:00 2001 From: Timor Gruber Date: Sun, 4 Oct 2026 01:33:30 +0300 Subject: [PATCH 2/4] enforce the strict output style in OpenCode Load the shared style as global OpenCode guidance and reinforce its response contract before every agent model request. --- private_dot_config/opencode/AGENTS.md.tmpl | 1 + .../opencode/plugins/strict-output-style.ts | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 private_dot_config/opencode/plugins/strict-output-style.ts diff --git a/private_dot_config/opencode/AGENTS.md.tmpl b/private_dot_config/opencode/AGENTS.md.tmpl index 19f208c..ca5f51b 100644 --- a/private_dot_config/opencode/AGENTS.md.tmpl +++ b/private_dot_config/opencode/AGENTS.md.tmpl @@ -14,3 +14,4 @@ delegation. {{ template "global-ai-workflow-guidelines.md.tmpl" . }} +{{ template "strict-output-style.md.tmpl" . -}} diff --git a/private_dot_config/opencode/plugins/strict-output-style.ts b/private_dot_config/opencode/plugins/strict-output-style.ts new file mode 100644 index 0000000..7420790 --- /dev/null +++ b/private_dot_config/opencode/plugins/strict-output-style.ts @@ -0,0 +1,29 @@ +const reminder = `Strict output-style reminder (conversational replies only, never inside files): +Use exactly these five sections, with these headings and emojis, in this order, and no others: +1. 🔍 Findings/Context +2. ⚠️ Limits/Risks +3. ✅ Done +4. ✋ Recommendations +5. ➡️ Next +Findings holds only facts about the current state, never a proposed change. Recommendations holds every proposal in full (a plan or set of edits awaiting approval, even if it is the only option; which option you favor and why; optional follow-ups); Next is required actions and decisions, such as the approval request for that plan. A required decision you also have an opinion on stays as one Next item with the pick stated inline. +Omit a section only when it is empty. Do not rename, merge, or invent sections. Keep the whole block together at the end of the reply. Escape hatch: a one or two line answer may drop all five headings together, never a subset.` + +type ContextEvent = { + system: Array<{ type: "text"; text: string }> +} + +type PluginContext = { + session: { + hook(name: "context", handler: (event: ContextEvent) => void): Promise + } +} + +export default { + id: "strict-output-style", + + async setup(ctx: PluginContext) { + await ctx.session.hook("context", event => { + event.system.push({ type: "text", text: reminder }) + }) + }, +} From d542ba7830c9be31c5978c009e5436af7d9663c1 Mon Sep 17 00:00:00 2001 From: Timor Gruber Date: Sun, 4 Oct 2026 01:33:52 +0300 Subject: [PATCH 3/4] enforce the strict output style in Codex Load the shared style in Codex and merge a per-prompt reminder hook without replacing unrelated user hook configuration. --- dot_codex/AGENTS.md.tmpl | 1 + ...xecutable_reinforce-strict-output-style.sh | 16 +++ run_after_ensure-codex-hooks.sh | 118 ++++++++++++++++++ 3 files changed, 135 insertions(+) create mode 100644 dot_codex/hooks/executable_reinforce-strict-output-style.sh create mode 100644 run_after_ensure-codex-hooks.sh diff --git a/dot_codex/AGENTS.md.tmpl b/dot_codex/AGENTS.md.tmpl index e32d2b6..13bdbd2 100644 --- a/dot_codex/AGENTS.md.tmpl +++ b/dot_codex/AGENTS.md.tmpl @@ -18,4 +18,5 @@ unavailable. {{ template "global-ai-workflow-guidelines.md.tmpl" . }} +{{ template "strict-output-style.md.tmpl" . }} @~/.codex/RTK.md diff --git a/dot_codex/hooks/executable_reinforce-strict-output-style.sh b/dot_codex/hooks/executable_reinforce-strict-output-style.sh new file mode 100644 index 0000000..9ade5af --- /dev/null +++ b/dot_codex/hooks/executable_reinforce-strict-output-style.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# Reinforce the Strict output style on every Codex user turn. +set -euo pipefail + +reminder="Strict output-style reminder (conversational replies only, never inside files): +Use exactly these five sections, with these headings and emojis, in this order, and no others: +1. 🔍 Findings/Context +2. ⚠️ Limits/Risks +3. ✅ Done +4. ✋ Recommendations +5. ➡️ Next +Findings holds only facts about the current state, never a proposed change. Recommendations holds every proposal in full (a plan or set of edits awaiting approval, even if it is the only option; which option you favor and why; optional follow-ups); Next is required actions and decisions, such as the approval request for that plan. A required decision you also have an opinion on stays as one Next item with the pick stated inline. +Omit a section only when it is empty. Do not rename, merge, or invent sections. Keep the whole block together at the end of the reply. Escape hatch: a one or two line answer may drop all five headings together, never a subset." + +jq -cn --arg ctx "$reminder" \ + '{hookSpecificOutput: {hookEventName: "UserPromptSubmit", additionalContext: $ctx}}' diff --git a/run_after_ensure-codex-hooks.sh b/run_after_ensure-codex-hooks.sh new file mode 100644 index 0000000..bf8d782 --- /dev/null +++ b/run_after_ensure-codex-hooks.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Register the Strict output-style reminder without owning Codex's hooks file. +# Existing hooks and unrelated configuration remain unchanged. + +CONFIG_DIR="$HOME/.codex" +CONFIG_FILE="$CONFIG_DIR/hooks.json" +# Codex expands HOME when it runs the registered command. +# shellcheck disable=SC2016 +HOOK_COMMAND='"$HOME/.codex/hooks/reinforce-strict-output-style.sh"' + +if ! command -v codex &>/dev/null; then + exit 0 +fi + +if ! command -v jq &>/dev/null; then + echo "Cannot merge Codex hooks: jq is not installed." >&2 + exit 1 +fi + +mkdir -p "$CONFIG_DIR" + +if [[ -f "$CONFIG_FILE" ]]; then + if ! jq -e ' + type == "object" and + ( + if has("hooks") then + (.hooks | type == "object") and + ( + if (.hooks | has("UserPromptSubmit")) then + (.hooks.UserPromptSubmit | type == "array") and + all( + .hooks.UserPromptSubmit[]; + type == "object" and + ((has("hooks") | not) or (.hooks | type == "array")) + ) + else + true + end + ) + else + true + end + ) + ' "$CONFIG_FILE" >/dev/null; then + echo "Cannot merge Codex hooks: $CONFIG_FILE has an unsupported structure." >&2 + exit 1 + fi + + matching_count="$( + jq --arg command "$HOOK_COMMAND" ' + [ + .hooks.UserPromptSubmit[]?.hooks[]? + | select(.type == "command" and .command == $command) + ] + | length + ' "$CONFIG_FILE" + )" + exact_count="$( + jq --arg command "$HOOK_COMMAND" ' + [ + .hooks.UserPromptSubmit[]?.hooks[]? + | select(. == {type: "command", command: $command, timeout: 5}) + ] + | length + ' "$CONFIG_FILE" + )" + + if [[ "$matching_count" == "1" && "$exact_count" == "1" ]]; then + exit 0 + fi +fi + +tmp="$(mktemp "${CONFIG_FILE}.tmp.XXXXXX")" +trap 'rm -f "$tmp"' EXIT + +# Dollar-prefixed names in this filter are jq variables. +# shellcheck disable=SC2016 +merge_filter=' + .hooks //= {} | + .hooks.UserPromptSubmit //= [] | + .hooks.UserPromptSubmit |= ( + map( + if ((.hooks // []) | any(.[]; .type == "command" and .command == $command)) then + .hooks |= map(select(.type != "command" or .command != $command)) | + if (.hooks | length) == 0 then empty else . end + else + . + end + ) + [ + { + hooks: [ + { + type: "command", + command: $command, + timeout: 5 + } + ] + } + ] + ) +' + +if [[ -f "$CONFIG_FILE" ]]; then + jq --arg command "$HOOK_COMMAND" "$merge_filter" "$CONFIG_FILE" >"$tmp" + + if chmod --reference="$CONFIG_FILE" "$tmp" 2>/dev/null; then + : + else + chmod "$(stat -f '%Lp' "$CONFIG_FILE")" "$tmp" + fi +else + jq -n --arg command "$HOOK_COMMAND" "$merge_filter" >"$tmp" +fi + +mv "$tmp" "$CONFIG_FILE" +trap - EXIT From bc4918a2d6a53c910b5dd6eeb28ce08862269cc3 Mon Sep 17 00:00:00 2001 From: Timor Gruber Date: Sun, 4 Oct 2026 01:44:04 +0300 Subject: [PATCH 4/4] reinforce git consent across agent runtimes Inject the existing commit and push consent reminder into OpenCode and Codex while preserving unrelated Codex hook configuration. --- .../hooks/executable_reinforce-git-consent.sh | 12 ++++ .../opencode/plugins/git-consent.ts | 25 +++++++ run_after_ensure-codex-hooks.sh | 67 +++++++++---------- 3 files changed, 69 insertions(+), 35 deletions(-) create mode 100644 dot_codex/hooks/executable_reinforce-git-consent.sh create mode 100644 private_dot_config/opencode/plugins/git-consent.ts diff --git a/dot_codex/hooks/executable_reinforce-git-consent.sh b/dot_codex/hooks/executable_reinforce-git-consent.sh new file mode 100644 index 0000000..411afa1 --- /dev/null +++ b/dot_codex/hooks/executable_reinforce-git-consent.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# Reinforce the commit and push consent rule on every Codex user turn. +set -euo pipefail + +reminder="Git consent reminder: +Commit or push only when the user asks for it as part of the current task (an ongoing piece of work, not a single agent run). +Permission lasts until that task ends and does not carry over to a later task, even in the same session. When unsure whether the task has changed, ask. +A commit request does not include pushing; a push needs its own request. +A request to create a pull request on an unpushed branch allows exactly one push to publish that branch; later pushes need a new request." + +jq -cn --arg ctx "$reminder" \ + '{hookSpecificOutput: {hookEventName: "UserPromptSubmit", additionalContext: $ctx}}' diff --git a/private_dot_config/opencode/plugins/git-consent.ts b/private_dot_config/opencode/plugins/git-consent.ts new file mode 100644 index 0000000..1d6a770 --- /dev/null +++ b/private_dot_config/opencode/plugins/git-consent.ts @@ -0,0 +1,25 @@ +const reminder = `Git consent reminder: +Commit or push only when the user asks for it as part of the current task (an ongoing piece of work, not a single agent run). +Permission lasts until that task ends and does not carry over to a later task, even in the same session. When unsure whether the task has changed, ask. +A commit request does not include pushing; a push needs its own request. +A request to create a pull request on an unpushed branch allows exactly one push to publish that branch; later pushes need a new request.` + +type ContextEvent = { + system: Array<{ type: "text"; text: string }> +} + +type PluginContext = { + session: { + hook(name: "context", handler: (event: ContextEvent) => void): Promise + } +} + +export default { + id: "git-consent", + + async setup(ctx: PluginContext) { + await ctx.session.hook("context", event => { + event.system.push({ type: "text", text: reminder }) + }) + }, +} diff --git a/run_after_ensure-codex-hooks.sh b/run_after_ensure-codex-hooks.sh index bf8d782..de31be3 100644 --- a/run_after_ensure-codex-hooks.sh +++ b/run_after_ensure-codex-hooks.sh @@ -1,14 +1,17 @@ #!/usr/bin/env bash set -euo pipefail -# Register the Strict output-style reminder without owning Codex's hooks file. -# Existing hooks and unrelated configuration remain unchanged. +# Register global reminder hooks without owning Codex's hooks file. Existing +# hooks and unrelated configuration remain unchanged. CONFIG_DIR="$HOME/.codex" CONFIG_FILE="$CONFIG_DIR/hooks.json" -# Codex expands HOME when it runs the registered command. +# Codex expands HOME when it runs the registered commands. # shellcheck disable=SC2016 -HOOK_COMMAND='"$HOME/.codex/hooks/reinforce-strict-output-style.sh"' +HOOK_COMMANDS=( + '"$HOME/.codex/hooks/reinforce-strict-output-style.sh"' + '"$HOME/.codex/hooks/reinforce-git-consent.sh"' +) if ! command -v codex &>/dev/null; then exit 0 @@ -48,26 +51,22 @@ if [[ -f "$CONFIG_FILE" ]]; then exit 1 fi - matching_count="$( - jq --arg command "$HOOK_COMMAND" ' - [ - .hooks.UserPromptSubmit[]?.hooks[]? + if jq -e --args ' + . as $root | + $ARGS.positional as $commands | + all( + $commands[]; + . as $command | + ([ + $root.hooks.UserPromptSubmit[]?.hooks[]? | select(.type == "command" and .command == $command) - ] - | length - ' "$CONFIG_FILE" - )" - exact_count="$( - jq --arg command "$HOOK_COMMAND" ' - [ - .hooks.UserPromptSubmit[]?.hooks[]? + ] | length) == 1 and + ([ + $root.hooks.UserPromptSubmit[]?.hooks[]? | select(. == {type: "command", command: $command, timeout: 5}) - ] - | length - ' "$CONFIG_FILE" - )" - - if [[ "$matching_count" == "1" && "$exact_count" == "1" ]]; then + ] | length) == 1 + ) + ' "${HOOK_COMMANDS[@]}" <"$CONFIG_FILE" >/dev/null; then exit 0 fi fi @@ -78,32 +77,30 @@ trap 'rm -f "$tmp"' EXIT # Dollar-prefixed names in this filter are jq variables. # shellcheck disable=SC2016 merge_filter=' + def is_owned($commands): + .type == "command" and + (.command as $command | $commands | index($command) != null); + + $ARGS.positional as $commands | .hooks //= {} | .hooks.UserPromptSubmit //= [] | .hooks.UserPromptSubmit |= ( map( - if ((.hooks // []) | any(.[]; .type == "command" and .command == $command)) then - .hooks |= map(select(.type != "command" or .command != $command)) | + if ((.hooks // []) | any(.[]; is_owned($commands))) then + .hooks |= map(select(is_owned($commands) | not)) | if (.hooks | length) == 0 then empty else . end else . end ) + [ - { - hooks: [ - { - type: "command", - command: $command, - timeout: 5 - } - ] - } + $commands[] | + {hooks: [{type: "command", command: ., timeout: 5}]} ] ) ' if [[ -f "$CONFIG_FILE" ]]; then - jq --arg command "$HOOK_COMMAND" "$merge_filter" "$CONFIG_FILE" >"$tmp" + jq --args "$merge_filter" "${HOOK_COMMANDS[@]}" <"$CONFIG_FILE" >"$tmp" if chmod --reference="$CONFIG_FILE" "$tmp" 2>/dev/null; then : @@ -111,7 +108,7 @@ if [[ -f "$CONFIG_FILE" ]]; then chmod "$(stat -f '%Lp' "$CONFIG_FILE")" "$tmp" fi else - jq -n --arg command "$HOOK_COMMAND" "$merge_filter" >"$tmp" + jq -n --args "$merge_filter" "${HOOK_COMMANDS[@]}" >"$tmp" fi mv "$tmp" "$CONFIG_FILE"