Skip to content
Closed
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
98 changes: 98 additions & 0 deletions .claude/hooks/rtk-rewrite.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#!/usr/bin/env bash
# rtk-hook-version: 3
# RTK Claude Code hook — rewrites commands to use rtk for token savings.
# Requires: rtk >= 0.23.0, jq
#
# This is a thin delegating hook: all rewrite logic lives in `rtk rewrite`,
# which is the single source of truth (src/discover/registry.rs).
# To add or change rewrite rules, edit the Rust registry — not this file.
#
# Exit code protocol for `rtk rewrite`:
# 0 + stdout Rewrite found, no deny/ask rule matched → auto-allow
# 1 No RTK equivalent → pass through unchanged
# 2 Deny rule matched → pass through (Claude Code native deny handles it)
# 3 + stdout Ask rule matched → rewrite but let Claude Code prompt the user

if ! command -v jq &>/dev/null; then
echo "[rtk] WARNING: jq is not installed. Hook cannot rewrite commands. Install jq: https://jqlang.github.io/jq/download/" >&2
exit 0
fi

if ! command -v rtk &>/dev/null; then
echo "[rtk] WARNING: rtk is not installed or not in PATH. Hook cannot rewrite commands. Install: https://github.com/rtk-ai/rtk#installation" >&2
exit 0
fi

# Version guard: rtk rewrite was added in 0.23.0.
# Older binaries: warn once and exit cleanly (no silent failure).
RTK_VERSION=$(rtk --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [ -n "$RTK_VERSION" ]; then
MAJOR=$(echo "$RTK_VERSION" | cut -d. -f1)
MINOR=$(echo "$RTK_VERSION" | cut -d. -f2)
# Require >= 0.23.0
if [ "$MAJOR" -eq 0 ] && [ "$MINOR" -lt 23 ]; then
echo "[rtk] WARNING: rtk $RTK_VERSION is too old (need >= 0.23.0). Upgrade: cargo install rtk" >&2
exit 0
fi
fi

INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')

if [ -z "$CMD" ]; then
exit 0
fi

# Delegate all rewrite + permission logic to the Rust binary.
REWRITTEN=$(rtk rewrite "$CMD" 2>/dev/null)
EXIT_CODE=$?

case $EXIT_CODE in
0)
# Rewrite found, no permission rules matched — safe to auto-allow.
# If the output is identical, the command was already using RTK.
[ "$CMD" = "$REWRITTEN" ] && exit 0
;;
1)
# No RTK equivalent — pass through unchanged.
exit 0
;;
2)
# Deny rule matched — let Claude Code's native deny rule handle it.
exit 0
;;
Comment on lines +60 to +63
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

For Claude Code PreToolUse hooks, is omitting permissionDecisiontreated as allow/pass-through, and is returningpermissionDecision: "deny" the correct way to enforce a deny decision from a hook?

💡 Result:

Yes, omitting permissionDecision (i.e., no JSON output from the PreToolUse hook, just exit 0) is treated as allow/pass-through to the native permission system. Returning permissionDecision: "deny" (in the correct format: {"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "..."} } with exit code 0) is the correct way to enforce a deny decision from a hook.

Citations:


RTK deny decision is not enforced in the deny branch.

On Line 60-63, when EXIT_CODE=2, the script exits with 0 and no output. This is treated as allow/pass-through by Claude Code's permission system. The RTK deny decision is effectively ignored.

To enforce the deny, output the correct JSON structure with permissionDecision: "deny":

🔒 Fix to enforce deny outcome
   2)
-    # Deny rule matched — let Claude Code's native deny rule handle it.
-    exit 0
+    # Deny rule matched — explicitly deny in hook output.
+    jq -n \
+      '{
+        "hookSpecificOutput": {
+          "hookEventName": "PreToolUse",
+          "permissionDecision": "deny",
+          "permissionDecisionReason": "Blocked by RTK deny rule"
+        }
+      }'
+    exit 0
     ;;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/hooks/rtk-rewrite.sh around lines 60 - 63, When the script hits the
deny branch (the case labelled "2)" where EXIT_CODE=2) it's currently exiting
with 0 and no output, so the RTK deny decision is ignored; change that branch to
write the required JSON response (e.g.,
{"permissionDecision":"deny","reason":"RTK denied"} or similar) to stdout so
Claude Code sees the deny, and then exit appropriately (keep the script’s
expected exit behavior but ensure the JSON is emitted). Update the "2)" case to
print the JSON object with permissionDecision:"deny" (using the same
variable/branch names EXIT_CODE and the case "2)") and then exit.

3)
# Ask rule matched — rewrite the command but do NOT auto-allow so that
# Claude Code prompts the user for confirmation.
;;
*)
exit 0
;;
esac

ORIGINAL_INPUT=$(echo "$INPUT" | jq -c '.tool_input')
UPDATED_INPUT=$(echo "$ORIGINAL_INPUT" | jq --arg cmd "$REWRITTEN" '.command = $cmd')

if [ "$EXIT_CODE" -eq 3 ]; then
# Ask: rewrite the command, omit permissionDecision so Claude Code prompts.
jq -n \
--argjson updated "$UPDATED_INPUT" \
'{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"updatedInput": $updated
}
}'
else
# Allow: rewrite the command and auto-allow.
jq -n \
--argjson updated "$UPDATED_INPUT" \
'{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "RTK auto-rewrite",
"updatedInput": $updated
}
}'
fi
15 changes: 15 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/rtk-rewrite.sh"
}
]
}
Comment on lines +1 to +12
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description focuses on model-id remapping in the proxy, but this PR also adds Claude Code/agent docs and hook configuration under .claude/ plus new guide files. If these additions are intentional, please mention them in the PR description; otherwise consider moving them to a separate PR to keep the fix scoped and easier to review/revert.

Copilot uses AI. Check for mistakes.
]
}
}
25 changes: 25 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# AGENTS.md — Claw Dev Agent Rules

## Tool Preferences

| Task | Use | Not |
|------|-----|-----|
| Project overview | `tokei src/ shared/` | `ls -la` |
| Find files | `fd -e ts src/` | `find src -name "*.ts"` |
| Search content | `rg 'pattern' src/ -t ts` | `grep -r` |
| Structural search | `ast-grep run --lang ts --pattern '...' src/` | complex regex |
| Query JSON | `jq '.key' file.json` | `cat file.json \| grep` |
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The “Not” example uses \| inside inline code (e.g., cat file.json \| grep). In a shell this escapes the pipe, so the example is not equivalent to cat file.json | grep. Use an unescaped | (or describe the pipe in prose) to avoid misleading copy/paste.

Suggested change
| Query JSON | `jq '.key' file.json` | `cat file.json \| grep` |
| Query JSON | `jq '.key' file.json` | `cat file.json | grep` |

Copilot uses AI. Check for mistakes.
| Replace strings | `sd 'old' 'new' file.ts` | `sed -i` |
| Run tests | `node --test tests/*.test.mjs` | — |

## Quick Reference

```bash
tokei src/ shared/ # codebase stats
fd -e ts src/ # find TS files
rg 'pattern' src/ -t ts -l # search content
ast-grep run --lang ts --pattern '...' src/ # structural search
jq '.dependencies' package.json # query JSON
sd 'old' 'new' file.ts # safe replace
node --test tests/*.test.mjs # run all tests
```
78 changes: 78 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Claw Dev — Developer Guide

## Supported Providers

| Provider | Env Key | Default Model |
|----------|---------|---------------|
| Anthropic | `ANTHROPIC_API_KEY` | `claude-sonnet-4-20250514` |
| OpenAI | `OPENAI_API_KEY` | `gpt-4.1-mini` |
| Gemini | `GEMINI_API_KEY` | `gemini-2.5-flash` |
| Groq | `GROQ_API_KEY` | `openai/gpt-oss-20b` |
| GitHub Copilot | `COPILOT_TOKEN` | `openai/gpt-4.1-mini` |
| z.ai | `ZAI_API_KEY` | `glm-5` |
| Ollama | local | `qwen3` |
Comment on lines +3 to +13
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Provider support table is out of sync with actual runtime support.

On Line 5-Line 13, the guide documents OpenAI/Groq/Copilot/z.ai/Ollama as supported, but runtime currently only accepts anthropic and gemini (src/config.ts Line 7-Line 15, Line 23-Line 52; src/providers.ts Line 11). This will lead users to invalid .env setups and startup failures.

🛠️ Proposed doc correction
 | Provider | Env Key | Default Model |
 |----------|---------|---------------|
 | Anthropic | `ANTHROPIC_API_KEY` | `claude-sonnet-4-20250514` |
-| OpenAI | `OPENAI_API_KEY` | `gpt-4.1-mini` |
 | Gemini | `GEMINI_API_KEY` | `gemini-2.5-flash` |
-| Groq | `GROQ_API_KEY` | `openai/gpt-oss-20b` |
-| GitHub Copilot | `COPILOT_TOKEN` | `openai/gpt-4.1-mini` |
-| z.ai | `ZAI_API_KEY` | `glm-5` |
-| Ollama | local | `qwen3` |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## Supported Providers
| Provider | Env Key | Default Model |
|----------|---------|---------------|
| Anthropic | `ANTHROPIC_API_KEY` | `claude-sonnet-4-20250514` |
| OpenAI | `OPENAI_API_KEY` | `gpt-4.1-mini` |
| Gemini | `GEMINI_API_KEY` | `gemini-2.5-flash` |
| Groq | `GROQ_API_KEY` | `openai/gpt-oss-20b` |
| GitHub Copilot | `COPILOT_TOKEN` | `openai/gpt-4.1-mini` |
| z.ai | `ZAI_API_KEY` | `glm-5` |
| Ollama | local | `qwen3` |
## Supported Providers
| Provider | Env Key | Default Model |
|----------|---------|---------------|
| Anthropic | `ANTHROPIC_API_KEY` | `claude-sonnet-4-20250514` |
| Gemini | `GEMINI_API_KEY` | `gemini-2.5-flash` |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLAUDE.md` around lines 3 - 13, The CLAUDE.md provider table is out of sync
with runtime validation (runtime currently only recognizes Anthropic and Gemini
via the provider whitelist in src/config.ts and src/providers.ts), so either
update the doc to list only Anthropic and Gemini with their correct env
keys/defaults or extend the runtime to accept the other providers; specifically,
fix the CLAUDE.md table rows (remove or correct OpenAI/Groq/Copilot/z.ai/Ollama
entries) to match the actual provider constants/allowedProviders in
src/config.ts and the provider resolution logic in src/providers.ts, or
alternatively add the missing provider entries and corresponding env key
handling in the provider constants and resolution functions so docs and runtime
match.


Copy `.env.example` to `.env` and fill in your keys.

## Codebase Overview

```bash
tokei src/ shared/ # instant language + line count
```

## Recommended Tools

All cross-platform (Linux / macOS / Windows).

| Tool | Install | Replaces | Gain |
|------|---------|----------|------|
| **rtk** | `curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh \| sh` | — | -60/90% tokens on bash output |
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The install command shows a literal \| inside inline code. In a shell, \| escapes the pipe and will not pipe to sh, so copy/paste will fail. Use an unescaped | (or move the command into a fenced code block) so the command works as written.

Suggested change
| **rtk** | `curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh \| sh` || -60/90% tokens on bash output |
| **rtk** | `curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh` || -60/90% tokens on bash output |

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Contributor docs recommend curl | sh from a mutable branch URL, creating avoidable remote code-execution/supply-chain risk.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At CLAUDE.md, line 29:

<comment>Contributor docs recommend `curl | sh` from a mutable branch URL, creating avoidable remote code-execution/supply-chain risk.</comment>

<file context>
@@ -0,0 +1,78 @@
+
+| Tool | Install | Replaces | Gain |
+|------|---------|----------|------|
+| **rtk** | `curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh \| sh` | — | -60/90% tokens on bash output |
+| **rg** | `apt install ripgrep` / `brew install ripgrep` | grep | Fastest content search |
+| **fd** | `apt install fd-find` / `brew install fd` | find | -99% output on targeted search |
</file context>
Fix with Cubic

| **rg** | `apt install ripgrep` / `brew install ripgrep` | grep | Fastest content search |
| **fd** | `apt install fd-find` / `brew install fd` | find | -99% output on targeted search |
| **ast-grep** | `npm install -g @ast-grep/cli` | regex grep | Semantic TS/JS code search |
| **jq** | `apt install jq` / `brew install jq` | grep on JSON | Precise JSON queries |
| **sd** | `cargo install sd` / `brew install sd` | sed | Cross-platform safe replace |
| **tokei** | `cargo install tokei` / `brew install tokei` | wc/ls | -75% vs ls for project overview |

## Tool Usage

### Project overview
```bash
tokei src/ shared/ # language breakdown
```

### Find files — fd over find
```bash
fd -e ts src/ # all .ts files
fd -g '*.test.mjs' tests/ # test files
# ❌ find src -name "*.ts" generates 43k chars for 1,332 files
```

### Search content — rg
```bash
rg 'provider' src/ -t ts # search TypeScript
rg 'GEMINI' .env.example # find env keys
```

### Structural search — ast-grep
```bash
ast-grep run --lang ts --pattern 'import { $A } from "$B"' src/
ast-grep run --lang ts --pattern 'z.string()' src/
```

### Query JSON/config
```bash
jq '.dependencies' package.json
jq 'keys' shared/providerModels.js 2>/dev/null
```

### Safe string replace — sd over sed
```bash
sd 'gemini-2.5-flash' 'gemini-3-flash' src/anthropicCompatProxy.ts
```

## Running Tests

```bash
node --test tests/*.test.mjs
```
12 changes: 12 additions & 0 deletions src/anthropicCompatProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,18 @@ function resolveRequestModel(body: AnthropicMessageRequest): string {
return requested;
}

// The bundled client may send a Claude model id (e.g. "claude-sonnet-4-20250514") or a
// short alias ("sonnet", "opus", "haiku") regardless of the active provider. Non-Anthropic
// backends do not recognise these names and return 404. Remap them to ACTIVE_MODEL so the
// correct provider model is used. The catalog check above means an operator can still
// override this by adding a claude-* id to their provider catalog explicitly.
const isClaude = requested.startsWith("claude-");
const isClaudeAlias = requested === "sonnet" || requested === "opus" || requested === "haiku";

if (isClaude || isClaudeAlias) {
return ACTIVE_MODEL;
}

// Allow custom model ids for user-managed providers even if they are not in the default catalog.
if (
PROVIDER === "ollama" ||
Expand Down