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
30 changes: 24 additions & 6 deletions lib/hook-utils.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1147,18 +1147,36 @@ hook::git_resolve_index() {
# -S/--split-string re-splits its operand into argv (GNU env), so a
# quoted 'git commit --no-verify' would otherwise hide from the
# resolver as one non-git word. Splice the split words back into the
# scan and restart at the command position. The splice drops every
# word before `i`, this `env` included, so a chdir already recorded for
# it is not re-walked and stays recorded — which is right, because env
# performs that chdir whether or not -S rewrites the command.
# scan and resume. The splice drops every word before `i`, this `env`
# included, so a chdir already recorded for it is not re-walked and
# stays recorded — which is right, because env performs that chdir
# whether or not -S rewrites the command.
#
# Resume INSIDE env's own option loop (`continue`, not `continue 2`),
# because the split words are env's OWN arguments: `-S` exists so a
# shebang line can carry env options, and GNU documents exactly that
# (`#!/usr/bin/env -S -i some-program`). Restarting at the command
# dispatcher instead read a leading option in the split string as the
# COMMAND NAME and abandoned the whole segment — `env -S '-C <dir> git
# push --force'` resolved to no git at all, so every guard skipped a
# real force-push, and `env -S '-C <sha256-repo> git push
# --force-with-lease=main:<40-hex>'` skipped a lease against a movable
# ref name. Staying in this loop also keeps `env_ci` in scope, so
# `env -C a -S '-C b git …'` is last-wins in the one slot GNU env
# keeps, exactly as an unspliced `env -C a -C b` already is.
#
# Termination: each splice consumes the `-S` word and its operand and
# substitutes only the operand's own words, so the argv's byte count
# strictly decreases — a self-referential `env -S '-S -S'` runs out
# rather than looping.
-S | --split-string)
local sval=""
((i + 1 < n)) && sval="${w[i + 1]}"
hook::env_s_split "$sval"
w=(${HOOK_ENV_S_WORDS[@]+"${HOOK_ENV_S_WORDS[@]}"} "${w[@]:i+2}")
n=${#w[@]}
i=0
continue 2
continue
;;
-S* | --split-string=*)
local sval="${etok#-S}"
Expand All @@ -1167,7 +1185,7 @@ hook::git_resolve_index() {
w=(${HOOK_ENV_S_WORDS[@]+"${HOOK_ENV_S_WORDS[@]}"} "${w[@]:i+1}")
n=${#w[@]}
i=0
continue 2
continue
;;
-C | --chdir)
((i + 1 < n)) && hook::wrapper_chdir_record env_ci "${w[i + 1]}"
Expand Down
20 changes: 20 additions & 0 deletions lib/hook-utils.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2048,6 +2048,26 @@ resolve_dirs_are "sudo --chdir=DIR reports the chdir" "other" sudo --chdir=other
resolve_dirs_are "sudo -C fd is not a chdir" "" sudo -C 3 git commit
# Nested wrappers each contribute, in execution order, for the caller to compose.
resolve_dirs_are "nested wrappers report both chdirs in order" "a|b" env -C a sudo -D b git commit
# `-S` exists so a shebang line can pass OPTIONS to env (`#!/usr/bin/env -S -i
# prog`), so the split words are env's own arguments and parsing must resume
# inside env's option loop. Resuming at the command dispatcher read a leading
# option in the split string as the COMMAND NAME and abandoned the segment
# entirely — the resolver reported no git, and every guard skipped the command.
resolve_dirs_are "env -S splices a chdir that belongs to env" "other" env -S '-C other git commit'
resolve_dirs_are "env --split-string= splices a chdir that belongs to env" "other" env --split-string='-C other git commit'
resolve_dirs_are "env -S with an attached operand splices the chdir" "other" env "-S-C other git commit"
resolve_dirs_are "env -S with no leading option still resolves git" "" env -S 'git commit'
# One env, one chdir slot: a -C inside the split string is last-wins against an
# earlier one outside it, not cumulative.
resolve_dirs_are "env -C first -S '-C second …' is last-wins in the one slot" "second" env -C first -S '-C second git commit'
# A valueless clustered option inside the split string must not swallow the chdir.
resolve_dirs_are "env -S '-v -C DIR git …' keeps the chdir" "other" env -S '-v -C other git commit'
# Termination: a self-referential -S consumes itself rather than looping.
if hook::git_resolve_index env -S '-S -S'; then
fail "env -S '-S -S' should resolve no git, resolved at $HOOK_GIT_RESOLVED_GI"
else
ok "a self-referential env -S terminates and resolves no git"
fi

# --- resolve_read_slice: shell fixed-point division ---------------------------
# The slice is produced by shell arithmetic rather than an awk spawn, and its
Expand Down
2 changes: 1 addition & 1 deletion plugins/actionlint/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "actionlint",
"version": "0.8.1",
"version": "0.8.2",
"description": "Lint GitHub Actions workflow files on edit via actionlint, surfacing findings as advisory context.",
"author": {
"name": "Melodic Software",
Expand Down
14 changes: 14 additions & 0 deletions plugins/actionlint/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@
All notable changes to the `actionlint` plugin are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning.

## [0.8.2]

### Fixed

- **Shared `hook-utils.sh`: `env -S` / `--split-string` no longer hides a whole command from the
git guards (#2124).** `-S` exists so a shebang line can pass OPTIONS to env
(`#!/usr/bin/env -S -i prog`), so the words it splits out are env's own arguments. The resolver
spliced them back into the scan but resumed at the COMMAND dispatcher, which read a leading
option in the split string as the command NAME and gave up — `env -S '-C <dir> git push --force'`
resolved to no git at all, so every guard built on `hook::git_resolve_index` skipped the command
unexamined. Parsing now resumes inside env's own option loop. That also keeps env's single chdir
slot last-wins across the splice, so `env -C a -S '-C b git …'` reports `b`, matching GNU env.
Synced from `lib/hook-utils.sh`.

## [0.8.1]

### Fixed
Expand Down
30 changes: 24 additions & 6 deletions plugins/actionlint/hooks/hook-utils.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1147,18 +1147,36 @@ hook::git_resolve_index() {
# -S/--split-string re-splits its operand into argv (GNU env), so a
# quoted 'git commit --no-verify' would otherwise hide from the
# resolver as one non-git word. Splice the split words back into the
# scan and restart at the command position. The splice drops every
# word before `i`, this `env` included, so a chdir already recorded for
# it is not re-walked and stays recorded — which is right, because env
# performs that chdir whether or not -S rewrites the command.
# scan and resume. The splice drops every word before `i`, this `env`
# included, so a chdir already recorded for it is not re-walked and
# stays recorded — which is right, because env performs that chdir
# whether or not -S rewrites the command.
#
# Resume INSIDE env's own option loop (`continue`, not `continue 2`),
# because the split words are env's OWN arguments: `-S` exists so a
# shebang line can carry env options, and GNU documents exactly that
# (`#!/usr/bin/env -S -i some-program`). Restarting at the command
# dispatcher instead read a leading option in the split string as the
# COMMAND NAME and abandoned the whole segment — `env -S '-C <dir> git
# push --force'` resolved to no git at all, so every guard skipped a
# real force-push, and `env -S '-C <sha256-repo> git push
# --force-with-lease=main:<40-hex>'` skipped a lease against a movable
# ref name. Staying in this loop also keeps `env_ci` in scope, so
# `env -C a -S '-C b git …'` is last-wins in the one slot GNU env
# keeps, exactly as an unspliced `env -C a -C b` already is.
#
# Termination: each splice consumes the `-S` word and its operand and
# substitutes only the operand's own words, so the argv's byte count
# strictly decreases — a self-referential `env -S '-S -S'` runs out
# rather than looping.
-S | --split-string)
local sval=""
((i + 1 < n)) && sval="${w[i + 1]}"
hook::env_s_split "$sval"
w=(${HOOK_ENV_S_WORDS[@]+"${HOOK_ENV_S_WORDS[@]}"} "${w[@]:i+2}")
n=${#w[@]}
i=0
continue 2
continue
;;
-S* | --split-string=*)
local sval="${etok#-S}"
Expand All @@ -1167,7 +1185,7 @@ hook::git_resolve_index() {
w=(${HOOK_ENV_S_WORDS[@]+"${HOOK_ENV_S_WORDS[@]}"} "${w[@]:i+1}")
n=${#w[@]}
i=0
continue 2
continue
;;
-C | --chdir)
((i + 1 < n)) && hook::wrapper_chdir_record env_ci "${w[i + 1]}"
Expand Down
2 changes: 1 addition & 1 deletion plugins/autonomy/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "autonomy",
"version": "0.14.1",
"version": "0.14.2",
"description": "Governed autonomous agent operation: role-topology, binding-seam, wiring-vs-advisor, telemetry, return-accounting, trigger-dispatch, per-work-class guardrail-matrix, standing-routine-catalog, and design-only runner-charter contracts for climbing the AI-adoption ladder, plus a guided-setup skill that discovers an adopting org's state, writes its schema-versioned binding, wires standards-pinned OTLP emission with a zero-cost file-artifact default, wires human-attested return capture at the task boundary, wires signal adapters with one governed dispatch entrypoint, binds the five-class guardrail matrix to an org's isolation substrates with an in-boundary live-validation probe before recording each fail-closed binding, and stands up standing-routine-catalog classes as scheduled temporal signal adapters behind the one governed queue with free scheduling defaults wired as reviewable changes and each routine's work-class mapping homed on the security surface.",
"author": {
"name": "Melodic Software",
Expand Down
14 changes: 14 additions & 0 deletions plugins/autonomy/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ All notable changes to the `autonomy` plugin are documented here. Format follows
Versions 0.1.0–0.7.0 predate this file (introduced with 0.7.1); their history lives in the
merged work-package PRs (#333, #343, #356, #372, #377, #600, #676).

## [0.14.2]

### Fixed

- **Shared `hook-utils.sh`: `env -S` / `--split-string` no longer hides a whole command from the
git guards (#2124).** `-S` exists so a shebang line can pass OPTIONS to env
(`#!/usr/bin/env -S -i prog`), so the words it splits out are env's own arguments. The resolver
spliced them back into the scan but resumed at the COMMAND dispatcher, which read a leading
option in the split string as the command NAME and gave up — `env -S '-C <dir> git push --force'`
resolved to no git at all, so every guard built on `hook::git_resolve_index` skipped the command
unexamined. Parsing now resumes inside env's own option loop. That also keeps env's single chdir
slot last-wins across the splice, so `env -C a -S '-C b git …'` reports `b`, matching GNU env.
Synced from `lib/hook-utils.sh`.

## [0.14.1]

### Fixed
Expand Down
30 changes: 24 additions & 6 deletions plugins/autonomy/hooks/hook-utils.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1147,18 +1147,36 @@ hook::git_resolve_index() {
# -S/--split-string re-splits its operand into argv (GNU env), so a
# quoted 'git commit --no-verify' would otherwise hide from the
# resolver as one non-git word. Splice the split words back into the
# scan and restart at the command position. The splice drops every
# word before `i`, this `env` included, so a chdir already recorded for
# it is not re-walked and stays recorded — which is right, because env
# performs that chdir whether or not -S rewrites the command.
# scan and resume. The splice drops every word before `i`, this `env`
# included, so a chdir already recorded for it is not re-walked and
# stays recorded — which is right, because env performs that chdir
# whether or not -S rewrites the command.
#
# Resume INSIDE env's own option loop (`continue`, not `continue 2`),
# because the split words are env's OWN arguments: `-S` exists so a
# shebang line can carry env options, and GNU documents exactly that
# (`#!/usr/bin/env -S -i some-program`). Restarting at the command
# dispatcher instead read a leading option in the split string as the
# COMMAND NAME and abandoned the whole segment — `env -S '-C <dir> git
# push --force'` resolved to no git at all, so every guard skipped a
# real force-push, and `env -S '-C <sha256-repo> git push
# --force-with-lease=main:<40-hex>'` skipped a lease against a movable
# ref name. Staying in this loop also keeps `env_ci` in scope, so
# `env -C a -S '-C b git …'` is last-wins in the one slot GNU env
# keeps, exactly as an unspliced `env -C a -C b` already is.
#
# Termination: each splice consumes the `-S` word and its operand and
# substitutes only the operand's own words, so the argv's byte count
# strictly decreases — a self-referential `env -S '-S -S'` runs out
# rather than looping.
-S | --split-string)
local sval=""
((i + 1 < n)) && sval="${w[i + 1]}"
hook::env_s_split "$sval"
w=(${HOOK_ENV_S_WORDS[@]+"${HOOK_ENV_S_WORDS[@]}"} "${w[@]:i+2}")
n=${#w[@]}
i=0
continue 2
continue
;;
-S* | --split-string=*)
local sval="${etok#-S}"
Expand All @@ -1167,7 +1185,7 @@ hook::git_resolve_index() {
w=(${HOOK_ENV_S_WORDS[@]+"${HOOK_ENV_S_WORDS[@]}"} "${w[@]:i+1}")
n=${#w[@]}
i=0
continue 2
continue
;;
-C | --chdir)
((i + 1 < n)) && hook::wrapper_chdir_record env_ci "${w[i + 1]}"
Expand Down
2 changes: 1 addition & 1 deletion plugins/bash-format/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "bash-format",
"version": "0.7.1",
"version": "0.7.2",
"description": "Auto-format and lint shell scripts on edit via shfmt + ShellCheck, using the consuming repo's own .editorconfig and .shellcheckrc.",
"author": {
"name": "Melodic Software",
Expand Down
14 changes: 14 additions & 0 deletions plugins/bash-format/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@
All notable changes to the `bash-format` plugin are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning.

## [0.7.2]

### Fixed

- **Shared `hook-utils.sh`: `env -S` / `--split-string` no longer hides a whole command from the
git guards (#2124).** `-S` exists so a shebang line can pass OPTIONS to env
(`#!/usr/bin/env -S -i prog`), so the words it splits out are env's own arguments. The resolver
spliced them back into the scan but resumed at the COMMAND dispatcher, which read a leading
option in the split string as the command NAME and gave up — `env -S '-C <dir> git push --force'`
resolved to no git at all, so every guard built on `hook::git_resolve_index` skipped the command
unexamined. Parsing now resumes inside env's own option loop. That also keeps env's single chdir
slot last-wins across the splice, so `env -C a -S '-C b git …'` reports `b`, matching GNU env.
Synced from `lib/hook-utils.sh`.

## [0.7.1]

### Fixed
Expand Down
30 changes: 24 additions & 6 deletions plugins/bash-format/hooks/hook-utils.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1147,18 +1147,36 @@ hook::git_resolve_index() {
# -S/--split-string re-splits its operand into argv (GNU env), so a
# quoted 'git commit --no-verify' would otherwise hide from the
# resolver as one non-git word. Splice the split words back into the
# scan and restart at the command position. The splice drops every
# word before `i`, this `env` included, so a chdir already recorded for
# it is not re-walked and stays recorded — which is right, because env
# performs that chdir whether or not -S rewrites the command.
# scan and resume. The splice drops every word before `i`, this `env`
# included, so a chdir already recorded for it is not re-walked and
# stays recorded — which is right, because env performs that chdir
# whether or not -S rewrites the command.
#
# Resume INSIDE env's own option loop (`continue`, not `continue 2`),
# because the split words are env's OWN arguments: `-S` exists so a
# shebang line can carry env options, and GNU documents exactly that
# (`#!/usr/bin/env -S -i some-program`). Restarting at the command
# dispatcher instead read a leading option in the split string as the
# COMMAND NAME and abandoned the whole segment — `env -S '-C <dir> git
# push --force'` resolved to no git at all, so every guard skipped a
# real force-push, and `env -S '-C <sha256-repo> git push
# --force-with-lease=main:<40-hex>'` skipped a lease against a movable
# ref name. Staying in this loop also keeps `env_ci` in scope, so
# `env -C a -S '-C b git …'` is last-wins in the one slot GNU env
# keeps, exactly as an unspliced `env -C a -C b` already is.
#
# Termination: each splice consumes the `-S` word and its operand and
# substitutes only the operand's own words, so the argv's byte count
# strictly decreases — a self-referential `env -S '-S -S'` runs out
# rather than looping.
-S | --split-string)
local sval=""
((i + 1 < n)) && sval="${w[i + 1]}"
hook::env_s_split "$sval"
w=(${HOOK_ENV_S_WORDS[@]+"${HOOK_ENV_S_WORDS[@]}"} "${w[@]:i+2}")
n=${#w[@]}
i=0
continue 2
continue
;;
-S* | --split-string=*)
local sval="${etok#-S}"
Expand All @@ -1167,7 +1185,7 @@ hook::git_resolve_index() {
w=(${HOOK_ENV_S_WORDS[@]+"${HOOK_ENV_S_WORDS[@]}"} "${w[@]:i+1}")
n=${#w[@]}
i=0
continue 2
continue
;;
-C | --chdir)
((i + 1 < n)) && hook::wrapper_chdir_record env_ci "${w[i + 1]}"
Expand Down
2 changes: 1 addition & 1 deletion plugins/biome-format/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "biome-format",
"version": "0.6.1",
"version": "0.6.2",
"description": "Auto-format and lint JS/TS/JSX/JSON on edit via Biome, only when a biome.json governs the repo — using the consuming repo's own Biome config.",
"author": {
"name": "Melodic Software",
Expand Down
14 changes: 14 additions & 0 deletions plugins/biome-format/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@
All notable changes to the `biome-format` plugin are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning.

## [0.6.2]

### Fixed

- **Shared `hook-utils.sh`: `env -S` / `--split-string` no longer hides a whole command from the
git guards (#2124).** `-S` exists so a shebang line can pass OPTIONS to env
(`#!/usr/bin/env -S -i prog`), so the words it splits out are env's own arguments. The resolver
spliced them back into the scan but resumed at the COMMAND dispatcher, which read a leading
option in the split string as the command NAME and gave up — `env -S '-C <dir> git push --force'`
resolved to no git at all, so every guard built on `hook::git_resolve_index` skipped the command
unexamined. Parsing now resumes inside env's own option loop. That also keeps env's single chdir
slot last-wins across the splice, so `env -C a -S '-C b git …'` reports `b`, matching GNU env.
Synced from `lib/hook-utils.sh`.

## [0.6.1]

### Fixed
Expand Down
Loading