Skip to content

Conversation

@devaxai
Copy link

@devaxai devaxai commented Jan 13, 2026

Description

Adds a new extension "Vim Leader Key" that brings Vim-style hierarchical hotkeys to Raycast.
It allows users to create nested menus of shortcuts (e.g., 'c' for Calculator, 'a' then 'f' for Finder) that can launch apps, run commands, or open URLs.
Features include JSON import/export, timeout configuration, and a visual editor for key bindings.
Screencast:
(Drag and drop your screenshots here. If you recorded a short video/gif, that's even better, but screenshots are fine.)

vim-leader-key-1 vim-leader-key-2 vim-leader-key-3 vim-leader-key-4 vim-leader-key-5 vim-leader-key-6 vim-leader-key-7 vim-leader-key-8 vim-leader-key-9 vim-leader-key-10 vim-leader-key-11 vim-leader-key-12

Checklist:

@raycastbot raycastbot added the new extension Label for PRs with new extensions label Jan 13, 2026
@raycastbot
Copy link
Collaborator

Congratulations on your new Raycast extension! 🚀

Due to our current reduced availability, the initial review may take up to 10-15 business days.

Once the PR is approved and merged, the extension will be available on our Store.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Jan 13, 2026

Greptile Overview

Greptile Summary

This PR adds a new "Vim Leader Key" extension that brings Vim-style hierarchical hotkeys to Raycast. The extension allows users to create nested menus of shortcuts with single-key navigation, supporting applications, URLs, folders, and shell commands. It includes JSON import/export functionality, timeout configuration, and a visual editor.

Key Features

  • Hierarchical key binding system with nested groups
  • Multiple action types: applications, URLs, folders, and shell commands
  • Import/export configuration as JSON (compatible with mikker's Leader Key app)
  • Configurable auto-reset timeout
  • Search functionality with key sequence typing
  • Migration support from legacy flat key sequences

Issues Found

Critical Security Issues

actions.ts contains shell injection vulnerabilities in openApp() and openFolder() functions. These use string interpolation with execAsync() without proper escaping, allowing malicious paths containing quotes, backticks, or dollar signs to execute arbitrary commands. These should use Raycast's open() API instead of shell commands.

Syntax Issues

  1. leader-key.tsx manually defines a Preferences interface, which violates Raycast best practices. Preferences are auto-generated in raycast-env.d.ts and should be imported from there instead.
  2. storage.ts uses deprecated substr() method which should be replaced with substring() or slice().

Code Quality

The extension demonstrates good architecture with:

  • Clean separation of concerns (storage, actions, forms, types)
  • Proper TypeScript typing throughout
  • Good error handling and user feedback
  • Comprehensive validation for key conflicts
  • Well-structured state management in the main component
  • Excellent documentation and README

The forms, import/export functionality, and type definitions are well-implemented with no issues found.

Confidence Score: 2/5

  • This PR contains critical shell injection vulnerabilities that must be fixed before merging
  • Score reflects two critical security vulnerabilities in actions.ts (shell injection in openApp and openFolder functions) that could allow arbitrary command execution, plus two syntax issues (manual Preferences interface and deprecated substr usage). While the overall architecture and most of the code is well-implemented, the security issues are severe enough to warrant immediate attention before this extension can be safely published.
  • Pay critical attention to extensions/vim-leader-key/src/actions.ts for shell injection fixes, and review extensions/vim-leader-key/src/leader-key.tsx and extensions/vim-leader-key/src/storage.ts for syntax corrections

Important Files Changed

File Analysis

Filename Score Overview
extensions/vim-leader-key/src/leader-key.tsx 3/5 Main UI component with manual Preferences interface that should use auto-generated types. Otherwise well-structured with good state management.
extensions/vim-leader-key/src/storage.ts 3/5 Storage layer with deprecated substr() usage. Good migration logic and data handling, but needs syntax update.
extensions/vim-leader-key/src/actions.ts 1/5 Critical shell injection vulnerabilities in openApp and openFolder functions. Command execution uses unsafe string interpolation that must be fixed.
extensions/vim-leader-key/src/forms.tsx 5/5 Form components with proper validation, conflict detection, and user feedback. Well-implemented with no issues found.
extensions/vim-leader-key/src/types.ts 5/5 Clean type definitions with proper type guards and legacy migration types. No issues found.
extensions/vim-leader-key/package.json 5/5 Well-configured package.json with proper commands, preferences, and dependencies. Follows Raycast conventions.

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

4 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +38 to +41
interface Preferences {
enableTimeout: boolean;
timeoutSeconds: string;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Manual Preferences interface definitions are not needed and can cause type mismatches. Raycast auto-generates preference types in raycast-env.d.ts when the extension runs, which you should import instead.

Remove this manual interface definition and import the auto-generated Preferences type from @raycast/api:

Suggested change
interface Preferences {
enableTimeout: boolean;
timeoutSeconds: string;
}
// Remove this manual interface - preferences are auto-generated

After the extension is built, you can import preferences types that are auto-generated based on your package.json preferences configuration.

}

export function generateId(): string {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
Copy link
Contributor

Choose a reason for hiding this comment

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

substr() is deprecated in JavaScript. Use substring() or slice() instead.

Suggested change
return Date.now().toString(36) + Math.random().toString(36).substr(2);
return Date.now().toString(36) + Math.random().toString(36).substring(2);

Comment on lines +54 to +60
async function openApp(appPath: string): Promise<void> {
if (appPath.endsWith(".app")) {
await execAsync(`open "${appPath}"`);
} else {
await execAsync(`open -a "${appPath}"`);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

The command execution here is vulnerable to shell injection if the appPath contains special characters like quotes, backticks, or dollar signs. While double quotes provide some protection, they don't escape all dangerous characters.

For example, if appPath is test"$(malicious)", the command would execute arbitrary code.

Consider using Raycast's open() API for opening applications instead of shell commands, or properly escape the path using a library that handles all shell metacharacters:

Suggested change
async function openApp(appPath: string): Promise<void> {
if (appPath.endsWith(".app")) {
await execAsync(`open "${appPath}"`);
} else {
await execAsync(`open -a "${appPath}"`);
}
}
async function openApp(appPath: string): Promise<void> {
// Use Raycast's open() API which is safer
if (appPath.endsWith(".app")) {
await open(appPath);
} else {
await open(appPath);
}
}

If you must use shell commands, use proper escaping or pass arguments as an array to avoid shell interpretation.

Comment on lines +66 to +68
async function openFolder(folderPath: string): Promise<void> {
await execAsync(`open "${folderPath}"`);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Same shell injection vulnerability as the openApp function. The folderPath is not properly escaped and can contain malicious characters.

Suggested change
async function openFolder(folderPath: string): Promise<void> {
await execAsync(`open "${folderPath}"`);
}
async function openFolder(folderPath: string): Promise<void> {
await open(folderPath); // Use Raycast's open() API instead
}

- update metadata images
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new extension Label for PRs with new extensions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants