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
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -594,13 +594,17 @@ Node-based `PreToolUse.cjs` bridge on Windows), then add this command hook to
{
"hooks": {
"PreToolUse": [{
"matcher": "^Bash$",
"matcher": "^run_shell_command$",
"hooks": [{"type": "command", "command": "/path/to/PreToolUse"}]
}]
}
}
```

The matcher uses Qwen's runtime shell tool ID. Compatibility aliases such as
`Bash` match exactly; anchoring that alias as `^Bash$` does not select the shell
tool.

Qwen sends the native JSON payload on standard input. The bridge returns its
native structured allow or deny decision and preserves the Controller reason.
See the official
Expand Down
2 changes: 1 addition & 1 deletion cmd/pitot/wire.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ func printWiringGuidance(host string, stdout io.Writer) error {
guidance := map[string]string{
"devin": "no hook configuration is required. Run:\n\n pitot dev --host devin -- devin -p \"<prompt>\"\n\nFor a separately managed runtime, use:\n\n pitot acp devin --runtime \"$PITOT_RUNTIME\" --prompt \"<prompt>\"",
"kimi": "add to ~/.kimi-code/config.toml (or $KIMI_CODE_HOME/config.toml):\n\n [[hooks]]\n event = \"PreToolUse\"\n matcher = \"Bash\"\n command = \"pitot hook kimi\"",
"qwen": "add to ~/.qwen/settings.json under hooks.PreToolUse:\n\n {\"matcher\": \"^Bash$\", \"hooks\": [{\"type\": \"command\", \"command\": \"<path to integrations/qwen/PreToolUse>\"}]}",
"qwen": "add to ~/.qwen/settings.json under hooks.PreToolUse:\n\n {\"matcher\": \"^run_shell_command$\", \"hooks\": [{\"type\": \"command\", \"command\": \"<path to integrations/qwen/PreToolUse>\"}]}",
"copilot": "add to ~/.copilot/settings.json under hooks.PreToolUse:\n\n {\"matcher\": \"Bash\", \"hooks\": [{\"type\": \"command\", \"command\": \"<path to integrations/copilot/PreToolUse>\"}]}",
"opencode": "register the OpenCode 2 plugin directory in ~/.config/opencode/opencode.json:\n\n {\"plugins\": [\"file:///<path to integrations/opencode>\"]}",
"pi": "copy integrations/pi/pitot.ts into ~/.pi/agent/extensions/ (or the repo's .pi/extensions/)",
Expand Down
37 changes: 37 additions & 0 deletions cmd/pitot/wire_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"os"
"path/filepath"
"regexp"
goruntime "runtime"
"strings"
"testing"
Expand Down Expand Up @@ -183,6 +184,42 @@ func TestWireHostGuidanceAndFlagRules(t *testing.T) {
}
}

func TestWireQwenGuidanceSelectsRuntimeShellTool(t *testing.T) {
t.Chdir(t.TempDir())
out, err := runInitHost(t, "qwen")
if err != nil {
t.Fatal(err)
}
var entry struct {
Matcher string `json:"matcher"`
Hooks []struct {
Type string `json:"type"`
Command string `json:"command"`
} `json:"hooks"`
}
found := false
for _, line := range strings.Split(out, "\n") {
if strings.HasPrefix(strings.TrimSpace(line), "{") {
if err := json.Unmarshal([]byte(line), &entry); err != nil {
t.Fatal(err)
}
found = true
}
}
if !found || entry.Matcher != "^run_shell_command$" || len(entry.Hooks) != 1 || entry.Hooks[0].Type != "command" || !strings.Contains(entry.Hooks[0].Command, "integrations/qwen/PreToolUse") {
t.Fatalf("invalid Qwen shell hook guidance: %s", out)
}
matcher := regexp.MustCompile(entry.Matcher)
for tool, want := range map[string]bool{"run_shell_command": true, "read_file": false, "monitor": false, "run_shell_command_extra": false} {
if got := matcher.MatchString(tool); got != want {
t.Errorf("matcher %q selects %q: got %v, want %v", entry.Matcher, tool, got, want)
}
}
if entries, err := os.ReadDir("."); err != nil || len(entries) != 0 {
t.Fatalf("user-level guidance wrote files: %v (error: %v)", entries, err)
}
}

func TestWireOpenCodeGuidanceUsesAPluginDirectory(t *testing.T) {
t.Chdir(t.TempDir())
out, err := runInitHost(t, "opencode")
Expand Down
Loading
Loading