Skip to content
Open
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
91 changes: 91 additions & 0 deletions hooks/cursor-rtk-rewrite.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* RTK Cursor Agent hook — Windows/Node.js version
* Use with --windows flag for cross-platform support
*/

const { execSync } = require('child_process');
const os = require('os');

// Platform-specific executable check
function commandExists(cmd) {
try {
const platform = os.platform();
if (platform === 'win32') {
execSync(`where ${cmd}`, { stdio: 'ignore' });
} else {
execSync(`command -v ${cmd}`, { stdio: 'ignore' });
}
return true;
} catch {
return false;
}
}

// Get rtk version
function getRtkVersion() {
try {
const output = execSync('rtk --version', { encoding: 'utf8' });
const match = output.match(/(\d+)\.(\d+)\.(\d+)/);
if (match) {
return { major: parseInt(match[1]), minor: parseInt(match[2]), full: match[0] };
}
} catch {}
return null;
}

// Main hook function
function preToolUse(context, toolName, toolInput) {
// Only process shell commands
if (toolName !== 'Bash' && toolName !== 'shell') {
return;
}

const command = toolInput?.command;
if (!command || typeof command !== 'string') {
return;
}

// Check if rtk is available
if (!commandExists('rtk')) {
console.error('[rtk] WARNING: rtk is not installed or not in PATH.');
console.error('[rtk] Install: https://github.com/rtk-ai/rtk#installation');
return;
}

// Version guard: rtk rewrite was added in 0.23.0
const version = getRtkVersion();
if (version && version.major === 0 && version.minor < 23) {
console.error(`[rtk] WARNING: rtk ${version.full} is too old (need >= 0.23.0).`);
console.error('[rtk] Upgrade: cargo install rtk');
return;
}

// Delegate all rewrite logic to the Rust binary
let rewritten;
try {
rewritten = execSync(`rtk rewrite ${JSON.stringify(command)}`, {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe']
}).trim();
} catch {
// rtk rewrite exits 1 when there's no rewrite — pass through silently
return;
}

// No change — nothing to do
if (rewritten === command) {
return;
}

// Return the rewritten command
return {
permission: 'allow',
permissionDecisionReason: 'RTK auto-rewrite',
updatedInput: {
...toolInput,
command: rewritten
}
};
}

module.exports = { preToolUse };
119 changes: 119 additions & 0 deletions hooks/rtk-rewrite.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/**
* RTK Auto-Rewrite Hook for Claude Code
* Windows/Node.js version - use with --windows flag
*
* This hook transparently rewrites raw commands to their rtk equivalents
* by intercepting PreToolUse events for Bash commands.
*/

function preToolUse(context, toolName, toolInput) {
// Only process Bash commands
if (toolName !== 'Bash') {
return;
}

const command = toolInput?.command;
if (!command || typeof command !== 'string') {
return;
}

// Skip if already using rtk
if (/^rtk\s/.test(command) || /\/rtk\s/.test(command)) {
return;
}

// Skip commands with heredocs (they break command parsing)
if (command.includes('<<')) {
return;
}

// Extract the first meaningful command (before pipes, &&, ||)
const firstCmd = command.split(/&&|\|\||\|/)[0].trim();

// Command rewrite rules
const rewrites = [
// --- Git commands ---
[/^git\s+status/, 'rtk git status'],
[/^git\s+diff/, 'rtk git diff'],
[/^git\s+log/, 'rtk git log'],
[/^git\s+add/, 'rtk git add'],
[/^git\s+commit/, 'rtk git commit'],
[/^git\s+push/, 'rtk git push'],
[/^git\s+pull/, 'rtk git pull'],
[/^git\s+branch/, 'rtk git branch'],
[/^git\s+fetch/, 'rtk git fetch'],
[/^git\s+stash/, 'rtk git stash'],
[/^git\s+show/, 'rtk git show'],

// --- GitHub CLI ---
[/^gh\s+/, 'rtk gh '],

// --- Cargo / Rust ---
[/^cargo\s+test/, 'rtk cargo test'],
[/^cargo\s+build/, 'rtk cargo build'],
[/^cargo\s+check/, 'rtk cargo check'],
[/^cargo\s+clippy/, 'rtk cargo clippy'],

// --- File operations ---
[/^cat\s+/, 'rtk read '],
[/^(rg|grep)\s+/, 'rtk grep '],
[/^ls\s?$/, 'rtk ls'],

// --- JavaScript/TypeScript tooling ---
[/^(pnpm\s+)?vitest\s/, 'rtk vitest run'],
[/^pnpm\s+test/, 'rtk vitest run'],
[/^(pnpm\s+)?tsc/, 'rtk tsc'],
[/^(npx\s+)?tsc/, 'rtk tsc'],
[/^pnpm\s+lint/, 'rtk lint'],
[/^(npx\s+)?eslint\s/, 'rtk lint'],
[/^(npx\s+)?prettier\s/, 'rtk prettier'],
[/^(npx\s+)?playwright\s/, 'rtk playwright'],
[/^pnpm\s+playwright/, 'rtk playwright'],
[/^(npx\s+)?prisma\s/, 'rtk prisma'],

// --- Package managers ---
[/^pnpm\s+(list|ls|outdated)/, 'rtk pnpm '],
[/^pnpm\s+install/, 'rtk pnpm install'],

// --- Containers & orchestration ---
[/^docker\s+(ps|images|logs)/, 'rtk docker '],
[/^kubectl\s+(get|logs)/, 'rtk kubectl '],

// --- Network ---
[/^curl\s+/, 'rtk curl '],

// --- Python tooling ---
[/^pytest\s/, 'rtk pytest'],
[/^python\s+-m\s+pytest/, 'rtk pytest'],
[/^ruff\s+/, 'rtk ruff '],
[/^pip\s+(list|outdated|install|show)/, 'rtk pip '],
[/^uv\s+pip\s+(list|outdated|install|show)/, 'rtk pip '],

// --- Go tooling ---
[/^go\s+test/, 'rtk go test'],
[/^go\s+build/, 'rtk go build'],
[/^go\s+vet/, 'rtk go vet'],
[/^golangci-lint/, 'rtk golangci-lint'],
];

// Find and apply the first matching rewrite
for (const [pattern, replacement] of rewrites) {
if (pattern.test(firstCmd)) {
const rewritten = command.replace(pattern, replacement);

return {
permissionDecision: 'allow',
permissionDecisionReason: 'RTK auto-rewrite',
updatedInput: {
...toolInput,
command: rewritten
}
};
}
}

// No rewrite needed, let command pass through unchanged
return undefined;
}

module.exports = { preToolUse };
Loading