Compare commits

...

5 Commits

Author SHA1 Message Date
Jesse Vincent
fd12428a5c docs(windows): document shell:bash hook dispatch and the PowerShell/CMD fallback hazards 2026-07-16 00:14:13 +00:00
Jesse Vincent
390bc07abb fix(hooks): dispatch the SessionStart hook via Git Bash on Windows
The SessionStart command string starts with a quoted path, which breaks
both Windows shells Claude Code may hand it to: PowerShell parses the
leading quoted string as an expression and dies on the next bareword
('Unexpected token session-start', #1751), and cmd.exe's /c quote rule
drops the outer quotes when the path contains a metacharacter, so a
profile dir like C:\Users\Name(External) truncates the command at the
'(' (#1918). Either way the bootstrap silently never loads.

Declare shell: "bash" on the hook. Claude Code >= 2.1.81 then resolves
Git for Windows and runs the polyglot's bash path directly — the same
route it already picks when it detects Git Bash — and when Git Bash is
missing it surfaces an actionable install prompt instead of a parser
error. Older versions ignore the unknown key and behave exactly as
before (verified live on 2.0.77 and 2.1.80).

Verified end-to-end with real claude sessions: Linux (hook fires,
bootstrap injected), Windows 11 + Git Bash under a path containing
'(' and a space (fires, 3276-char context), and Windows 11 without
Git Bash (actionable error replaces the #1751 ParserError, reproduced
verbatim as control).

Fixes #1751
Fixes #1918
2026-07-16 00:13:35 +00:00
Jesse Vincent
fb7b07088e docs: fix dead references to pruned claude-code-tools.md/copilot-tools.md
e7ddc25 deleted claude-code-tools.md and copilot-tools.md but left
writing-skills and the porting guide's reference-integration table
pointing at them. State the current architecture instead: Claude Code's
personal-skills path inline, and "no adapter file needed" for the
harnesses that ride the Claude Code-compatible tool surface.

Reported by @rasibintang (#1969, with a fix proposed in #1970).

Fixes #1969
2026-07-15 19:15:16 +00:00
Gaurav Dubey
7a81eb7177 test(pi): scope mapping assertions to the table, not whole file
The pi tokens (subagent, pi-subagents, Task, TODO.md) also appear in the
surrounding prose, so matching the whole file passed even with the mapping
table deleted — the exact regression this test exists to catch. Filter to
table rows (lines starting with '|') so the assertion fails when the table
is gone and passes on dev.

Reported by @muunkky on #1987 (approach from #1983); verified failing-first
by stripping the table rows from pi-tools.md.
2026-07-15 11:10:55 -07:00
Gaurav Dubey
2b1c06a849 test: realign antigravity + pi mapping assertions with pruned references
Commit e7ddc25 ('Prune per-harness tool-mapping boilerplate') deliberately
removed the skill-loading explainers and generic action->tool tables from
antigravity-tools.md and pi-tools.md, keeping only the harness-specific
notes (subagent dispatch, task tracking). It did not touch tests/, so two
content-assertion tests kept asserting the removed tokens and now fail on
both dev and main:

  - tests/antigravity/test-antigravity-tools.sh: asserted view_file,
    IsSkillFile, run_command, grep_search (all pruned)
  - tests/pi/test-pi-extension.mjs: asserted read/write/edit/bash (pruned)

Update both to assert only the surviving harness-specific mappings. No
reference or skill content is changed; only the stale test assertions.
2026-07-15 11:10:55 -07:00
7 changed files with 55 additions and 21 deletions

View File

@@ -784,10 +784,10 @@ Use this as the live index; when in doubt, read the files, not this table.
| Harness | Entry point | Bootstrap mechanism | Tool mapping | Tests | Distribution | | Harness | Entry point | Bootstrap mechanism | Tool mapping | Tests | Distribution |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| Claude Code | `.claude-plugin/plugin.json` + `hooks/hooks.json` | shell hook → `hooks/session-start` (`hookSpecificOutput.additionalContext`) | native `Skill` tool; `references/claude-code-tools.md` | `tests/hooks/` | marketplace | | Claude Code | `.claude-plugin/plugin.json` + `hooks/hooks.json` | shell hook → `hooks/session-start` (`hookSpecificOutput.additionalContext`) | native `Skill` tool; no adapter file needed | `tests/hooks/` | marketplace |
| Codex | `.codex-plugin/plugin.json` (declares empty `hooks`) | native skill discovery (no session-start hook) | `references/codex-tools.md` | `tests/codex/`, `tests/codex-plugin-sync/` | fork sync (`scripts/sync-to-codex-plugin.sh`) | | Codex | `.codex-plugin/plugin.json` (declares empty `hooks`) | native skill discovery (no session-start hook) | `references/codex-tools.md` | `tests/codex/`, `tests/codex-plugin-sync/` | fork sync (`scripts/sync-to-codex-plugin.sh`) |
| Cursor | `.cursor-plugin/plugin.json` + `hooks/hooks-cursor.json` | shell hook → `hooks/session-start` (`additional_context`) | `references/claude-code-tools.md` | `tests/hooks/` | hand-authored | | Cursor | `.cursor-plugin/plugin.json` + `hooks/hooks-cursor.json` | shell hook → `hooks/session-start` (`additional_context`) | none needed (Claude Codecompatible tool surface) | `tests/hooks/` | hand-authored |
| Copilot CLI | (shares Claude Code hook path; `COPILOT_CLI` env) | shell hook → `hooks/session-start` (`additionalContext`) | `references/copilot-tools.md` | `tests/hooks/` | — | | Copilot CLI | (shares Claude Code hook path; `COPILOT_CLI` env) | shell hook → `hooks/session-start` (`additionalContext`) | none needed (Claude Codecompatible tool surface) | `tests/hooks/` | — |
| Gemini CLI | `gemini-extension.json` + `GEMINI.md` | instructions file `@`-includes bootstrap + mapping | `references/gemini-tools.md` | — | `gemini extensions install` | | Gemini CLI | `gemini-extension.json` + `GEMINI.md` | instructions file `@`-includes bootstrap + mapping | `references/gemini-tools.md` | — | `gemini extensions install` |
| Kimi Code | `.kimi-plugin/plugin.json` | manifest `sessionStart.skill` loads `using-superpowers` | inline `skillInstructions` in manifest | `tests/kimi/` | marketplace or `/plugins install` GitHub URL | | Kimi Code | `.kimi-plugin/plugin.json` | manifest `sessionStart.skill` loads `using-superpowers` | inline `skillInstructions` in manifest | `tests/kimi/` | marketplace or `/plugins install` GitHub URL |
| OpenCode | `.opencode/plugins/superpowers.js` (declared via root `package.json` `main`) | in-process: `config` hook registers skills dir; `experimental.chat.messages.transform` injects user message | inline in `superpowers.js` | `tests/opencode/` | `opencode.json` plugin git URL | | OpenCode | `.opencode/plugins/superpowers.js` (declared via root `package.json` `main`) | in-process: `config` hook registers skills dir; `experimental.chat.messages.transform` injects user message | inline in `superpowers.js` | `tests/opencode/` | `opencode.json` plugin git URL |

View File

@@ -6,9 +6,18 @@ Claude Code plugins need hooks that work on Windows, macOS, and Linux. This docu
## The Problem ## The Problem
Claude Code runs hook commands through the system's default shell: Claude Code runs hook commands through a shell:
- **Windows**: CMD.exe
- **macOS/Linux**: bash or sh - **macOS/Linux**: bash or sh
- **Windows with Git Bash installed**: Git Bash
- **Windows without Git Bash**: PowerShell (older versions used CMD.exe)
Neither Windows fallback shell can parse our command string: PowerShell treats
a leading quoted path as a string expression and errors on the next bareword,
and CMD.exe's `/c` quoting rules strip the outer quotes when the path contains
a metacharacter such as `(`. Our hooks therefore declare `"shell": "bash"`
(supported since Claude Code 2.1.81; older versions ignore the key), which
forces the Git Bash route and, when Git Bash is absent, produces an actionable
"install Git for Windows" error instead of a shell parser failure.
This creates several challenges: This creates several challenges:
@@ -42,6 +51,7 @@ hooks/
{ {
"type": "command", "type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start", "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start",
"shell": "bash",
"async": false "async": false
} }
] ]

View File

@@ -7,6 +7,7 @@
{ {
"type": "command", "type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start", "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start",
"shell": "bash",
"async": false "async": false
} }
] ]

View File

@@ -9,7 +9,7 @@ description: Use when creating new skills, editing existing skills, or verifying
**Writing skills IS Test-Driven Development applied to process documentation.** **Writing skills IS Test-Driven Development applied to process documentation.**
**Personal skills live in your runtime's skills directory** — see [claude-code-tools.md](../using-superpowers/references/claude-code-tools.md), [codex-tools.md](../using-superpowers/references/codex-tools.md), [copilot-tools.md](../using-superpowers/references/copilot-tools.md), or [gemini-tools.md](../using-superpowers/references/gemini-tools.md) for the path on your runtime. Codex, Copilot CLI, and Gemini CLI all also recognize `~/.agents/skills/` as a cross-runtime alias. **Personal skills live in your runtime's skills directory** (`~/.claude/skills/` on Claude Code) — see [codex-tools.md](../using-superpowers/references/codex-tools.md) or [gemini-tools.md](../using-superpowers/references/gemini-tools.md) for the path on those runtimes. Codex, Copilot CLI, and Gemini CLI all also recognize `~/.agents/skills/` as a cross-runtime alias.
You write test cases (pressure scenarios with subagents), watch them fail (baseline behavior), write the skill (documentation), watch tests pass (agents comply), and refactor (close loopholes). You write test cases (pressure scenarios with subagents), watch them fail (baseline behavior), write the skill (documentation), watch tests pass (agents comply), and refactor (close loopholes).

View File

@@ -2,8 +2,9 @@
# Validate the Antigravity (agy) integration. agy installs the existing plugin # Validate the Antigravity (agy) integration. agy installs the existing plugin
# directly (`agy plugin install <repo-url>`): it loads the bundled skills and # directly (`agy plugin install <repo-url>`): it loads the bundled skills and
# runs the SessionStart hook for bootstrap, so there is no agy-specific scaffold # runs the SessionStart hook for bootstrap, so there is no agy-specific scaffold
# to test. What IS agy-specific is the tool mapping — agy has no `Skill` tool and # to test. What IS agy-specific is the tool mapping — subagent dispatch via
# loads skills by reading SKILL.md with view_file — and SKILL.md pointing at it. # invoke_subagent (self/research types) and task tracking via a task artifact —
# and SKILL.md pointing at it.
# #
# Mirrors tests/pi/test-pi-extension.mjs's "tools reference documents # Mirrors tests/pi/test-pi-extension.mjs's "tools reference documents
# harness-specific mappings" check. CI-safe: does not require `agy` installed. # harness-specific mappings" check. CI-safe: does not require `agy` installed.
@@ -22,16 +23,8 @@ echo "test-antigravity-tools: checking Antigravity tool mapping"
# --- Mapping exists --------------------------------------------------------- # --- Mapping exists ---------------------------------------------------------
[ -f "$MAPPING" ] || fail "tool mapping missing at $MAPPING" [ -f "$MAPPING" ] || fail "tool mapping missing at $MAPPING"
# --- Skill-load mechanism: view_file on SKILL.md (IsSkillFile), no Skill tool -
grep -qiE "view_file" "$MAPPING" \
|| fail "mapping does not document view_file as the file/skill-read tool"
grep -qiE "SKILL\.md" "$MAPPING" \
|| fail "mapping does not document reading SKILL.md as the skill-load path"
grep -q "IsSkillFile" "$MAPPING" \
|| fail "mapping does not document setting IsSkillFile when loading a skill"
# --- Core action→tool mappings are documented ------------------------------- # --- Core action→tool mappings are documented -------------------------------
for tool in write_to_file replace_file_content run_command grep_search invoke_subagent; do for tool in write_to_file replace_file_content invoke_subagent; do
grep -q "$tool" "$MAPPING" \ grep -q "$tool" "$MAPPING" \
|| fail "mapping does not document the '$tool' tool" || fail "mapping does not document the '$tool' tool"
done done
@@ -50,4 +43,4 @@ grep -qE 'ArtifactType.*task|task. artifact' "$MAPPING" \
grep -q "antigravity-tools.md" "$SKILL" \ grep -q "antigravity-tools.md" "$SKILL" \
|| fail "SKILL.md Platform Adaptation does not reference antigravity-tools.md" || fail "SKILL.md Platform Adaptation does not reference antigravity-tools.md"
echo "PASS: Antigravity tool mapping valid (view_file skill-load, agy tools, SKILL.md link)" echo "PASS: Antigravity tool mapping valid (subagent dispatch, task artifact, SKILL.md link)"

View File

@@ -143,6 +143,27 @@ for (const forbiddenText of forbiddenTexts) {
echo "SessionStart hook output tests" echo "SessionStart hook output tests"
# Registration shape: the hook must declare shell:"bash" so Claude Code on
# Windows dispatches via Git Bash (or fails with an actionable error) instead
# of PowerShell/cmd.exe, whose parsers break on the quoted command string
# (PowerShell ParserError; cmd.exe quote-stripping on paths with metacharacters).
if node -e '
const hooks = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
const entry = hooks.hooks.SessionStart[0].hooks[0];
if (entry.shell !== "bash") {
console.error(`SessionStart hook shell is ${JSON.stringify(entry.shell)}, expected "bash"`);
process.exit(1);
}
if (!/run-hook\.cmd" session-start$/.test(entry.command)) {
console.error(`unexpected SessionStart command shape: ${entry.command}`);
process.exit(1);
}
' "$REPO_ROOT/hooks/hooks.json"; then
pass "hooks.json registers SessionStart with shell:bash dispatch"
else
fail "hooks.json registers SessionStart with shell:bash dispatch"
fi
claude_home="$(make_home claude-code)" claude_home="$(make_home claude-code)"
assert_command_output \ assert_command_output \
"Claude Code emits nested SessionStart additionalContext" \ "Claude Code emits nested SessionStart additionalContext" \

View File

@@ -122,7 +122,16 @@ test('pi tools reference documents pi-specific mappings', async () => {
assert.equal(existsSync(piToolsPath), true, 'pi-tools.md should exist'); assert.equal(existsSync(piToolsPath), true, 'pi-tools.md should exist');
const text = await readFile(piToolsPath, 'utf8'); const text = await readFile(piToolsPath, 'utf8');
for (const expected of ['Skill', 'Task', 'TodoWrite', 'read', 'write', 'edit', 'bash']) { // Assert against the mapping-table rows only. The surrounding prose mentions
assert.match(text, new RegExp(expected)); // these same tokens, so matching the whole file would still pass if the table
} // were deleted — the exact regression this test exists to catch.
const rows = text.split('\n').filter((line) => line.startsWith('|'));
assert.ok(
rows.some((row) => /subagent/i.test(row)),
'mapping table documents subagent dispatch',
);
assert.ok(
rows.some((row) => /todo|task/i.test(row)),
'mapping table documents task tracking',
);
}); });