Skip to content

Create MultiLinks using clipboard contents/then turn bundle into quicklink w/2 clicks.#27284

Open
chucktaylor2024 wants to merge 3 commits intoraycast:mainfrom
chucktaylor2024:ext/fast-multilinks
Open

Create MultiLinks using clipboard contents/then turn bundle into quicklink w/2 clicks.#27284
chucktaylor2024 wants to merge 3 commits intoraycast:mainfrom
chucktaylor2024:ext/fast-multilinks

Conversation

@chucktaylor2024
Copy link
Copy Markdown

@chucktaylor2024 chucktaylor2024 commented Apr 19, 2026

Description:
Create multilinks using clipboard contents (up to 5 URLs at once), then turn the bundle into a QuickLink with 2 clicks.

CleanShot 2026-04-19 at 12 34 37

Screencast

Checklist

CleanShot 2026-04-19 at 12 34 00 CleanShot 2026-04-19 at 12 34 00 CleanShot 2026-04-19 at 12 34 18 CleanShot 2026-04-19 at 12 34 31

@raycastbot raycastbot added new extension Label for PRs with new extensions platform: macOS labels Apr 19, 2026
@raycastbot
Copy link
Copy Markdown
Collaborator

Congratulations on your new Raycast extension! 🚀

We're currently experiencing a high volume of incoming requests. As a result, 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.

@chucktaylor2024 chucktaylor2024 changed the title Add fast-multilinks extension Create MultiLinks using clipboard contents/then turn bundle into quicklink by pressing enter. Apr 19, 2026
@chucktaylor2024 chucktaylor2024 marked this pull request as ready for review April 19, 2026 18:40
@chucktaylor2024 chucktaylor2024 changed the title Create MultiLinks using clipboard contents/then turn bundle into quicklink by pressing enter. Create MultiLinks using clipboard contents/then turn bundle into quicklink w/2 clicks. Apr 19, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 19, 2026

Greptile Summary

This is a new extension that creates named groups of URLs from clipboard history and opens them all at once, with optional browser selection and Quicklink support. The core workflow is clean, but there is a shell injection vulnerability that must be fixed before merging.

  • P0 – Shell injection: open-group.tsx and list-groups.tsx both pass clipboard-sourced URLs and LocalStorage-read browser names directly into shell command strings via execAsync. A crafted URL can execute arbitrary shell commands. Replace exec/execAsync with execFile/execFileAsync to bypass the shell entirely.
  • P1 – Deeplink author slug: The hardcoded Chucktaylor identifier in deep-links may not match the lowercase-normalised slug Raycast uses at runtime, causing Quicklinks to silently fail.
  • Missing metadata/ folder: Both view-mode commands require a metadata/ folder with Raycast-styled screenshots for store publication.

Confidence Score: 2/5

Not safe to merge — shell injection vulnerability requires a fix before this can be published

A P0 shell injection issue affects two files and poses a real security risk to users. A P1 deeplink correctness issue and a missing metadata folder also need to be addressed before store publishing.

src/open-group.tsx and src/list-groups.tsx both require switching from exec to execFile; a metadata/ folder with store screenshots needs to be added

Security Review

  • Shell injection (src/open-group.tsx lines 47–50, src/list-groups.tsx lines 37–40): URLs and browser names stored in LocalStorage are interpolated directly into shell command strings via execAsync. A clipboard-sourced URL containing shell metacharacters can break out of the quoted argument and execute arbitrary commands on the user's machine.

Important Files Changed

Filename Overview
extensions/fast-multilinks/src/open-group.tsx No-view command that opens group URLs via shell exec — has a shell injection vulnerability from unescaped URL/browser arguments passed to execAsync
extensions/fast-multilinks/src/list-groups.tsx Main view for managing groups — same shell injection issue in openURL helper, plus hardcoded author slug in deeplink construction
extensions/fast-multilinks/src/create-group-from-clipboard.tsx Clipboard-based group creation flow; contains the same hardcoded author slug in deeplink but no direct shell execution
extensions/fast-multilinks/package.json Extension manifest with correct structure; @raycast/utils is listed as a dependency but never imported in source
extensions/fast-multilinks/eslint.config.js Standard ESLint v9 flat config using defineConfig from eslint/config with @raycast/eslint-config — correct pattern
extensions/fast-multilinks/CHANGELOG.md Initial changelog with correct {PR_MERGE_DATE} placeholder
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/fast-multilinks/src/open-group.tsx
Line: 45-51

Comment:
**Shell injection via unescaped URL/browser arguments**

`execAsync` runs through `/bin/sh`, so any clipboard-sourced URL containing shell metacharacters (e.g. `https://evil.com" && rm -rf ~/Documents && echo "`) will be interpreted by the shell. An attacker who controls the clipboard (e.g. via a web page with clipboard API access) can execute arbitrary commands. Replace `exec` with `execFile` which spawns the process directly without a shell:

```typescript
import { execFile } from "child_process";
import { promisify } from "util";
const execFileAsync = promisify(execFile);

// with browser:
await execFileAsync("open", ["-a", group.browser, url]);
// without browser:
await execFileAsync("open", [url]);
```

The same issue exists in `list-groups.tsx`'s `openURL` helper (lines 35–41).

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: extensions/fast-multilinks/src/list-groups.tsx
Line: 35-41

Comment:
**Shell injection via unescaped URL/browser arguments**

Same issue as `open-group.tsx`: both `url` and `browserName` are interpolated into a shell string without escaping. Although `browserName` is constrained to dropdown values at entry time, the values are persisted to and read from `LocalStorage` as plain strings, so they are still attacker-influenced. Use `execFile` to avoid the shell entirely:

```typescript
import { execFile } from "child_process";
import { promisify } from "util";
const execFileAsync = promisify(execFile);

const openURL = async (url: string, browserName?: string) => {
  if (browserName) {
    await execFileAsync("open", ["-a", browserName, url]);
  } else {
    await execFileAsync("open", [url]);
  }
};
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: extensions/fast-multilinks/src/list-groups.tsx
Line: 106

Comment:
**Hardcoded author slug in deeplink may not match published extension**

The deeplink hardcodes `Chucktaylor` as the author slug. Raycast typically normalises author identifiers to lowercase when resolving extension deep-links, so `raycast://extensions/Chucktaylor/...` may silently fail to open the `open-group` command after publishing. The same pattern appears in `create-group-from-clipboard.tsx` line 119. Verify the exact author slug used by Raycast for this extension (usually the lowercased `author` field) and update both files accordingly.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: extensions/fast-multilinks/package.json
Line: 44-46

Comment:
**Unused `@raycast/utils` dependency**

`@raycast/utils` is listed as a runtime dependency but is never imported in any file under `src/`. Remove it to avoid unnecessary bundle bloat.

```suggestion
  "dependencies": {
    "@raycast/api": "^1.104.12"
  },
```

**Rule Used:** What: Every dependency listed in package.json must... ([source](https://app.greptile.com/review/custom-context?memory=bffc60eb-f9f2-4219-b804-76e29e267d43))

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "Initial commit: Fast MultiLinks extensio..." | Re-trigger Greptile

Comment on lines +45 to +51
for (const url of group.urls) {
if (group.browser) {
await execAsync(`open -a "${group.browser}" "${url}"`);
} else {
await execAsync(`open "${url}"`);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P0 security Shell injection via unescaped URL/browser arguments

execAsync runs through /bin/sh, so any clipboard-sourced URL containing shell metacharacters (e.g. https://evil.com" && rm -rf ~/Documents && echo ") will be interpreted by the shell. An attacker who controls the clipboard (e.g. via a web page with clipboard API access) can execute arbitrary commands. Replace exec with execFile which spawns the process directly without a shell:

import { execFile } from "child_process";
import { promisify } from "util";
const execFileAsync = promisify(execFile);

// with browser:
await execFileAsync("open", ["-a", group.browser, url]);
// without browser:
await execFileAsync("open", [url]);

The same issue exists in list-groups.tsx's openURL helper (lines 35–41).

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/fast-multilinks/src/open-group.tsx
Line: 45-51

Comment:
**Shell injection via unescaped URL/browser arguments**

`execAsync` runs through `/bin/sh`, so any clipboard-sourced URL containing shell metacharacters (e.g. `https://evil.com" && rm -rf ~/Documents && echo "`) will be interpreted by the shell. An attacker who controls the clipboard (e.g. via a web page with clipboard API access) can execute arbitrary commands. Replace `exec` with `execFile` which spawns the process directly without a shell:

```typescript
import { execFile } from "child_process";
import { promisify } from "util";
const execFileAsync = promisify(execFile);

// with browser:
await execFileAsync("open", ["-a", group.browser, url]);
// without browser:
await execFileAsync("open", [url]);
```

The same issue exists in `list-groups.tsx`'s `openURL` helper (lines 35–41).

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +35 to +41
const openURL = async (url: string, browserName?: string) => {
if (browserName) {
await execAsync(`open -a "${browserName}" "${url}"`);
} else {
await execAsync(`open "${url}"`);
}
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P0 security Shell injection via unescaped URL/browser arguments

Same issue as open-group.tsx: both url and browserName are interpolated into a shell string without escaping. Although browserName is constrained to dropdown values at entry time, the values are persisted to and read from LocalStorage as plain strings, so they are still attacker-influenced. Use execFile to avoid the shell entirely:

import { execFile } from "child_process";
import { promisify } from "util";
const execFileAsync = promisify(execFile);

const openURL = async (url: string, browserName?: string) => {
  if (browserName) {
    await execFileAsync("open", ["-a", browserName, url]);
  } else {
    await execFileAsync("open", [url]);
  }
};
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/fast-multilinks/src/list-groups.tsx
Line: 35-41

Comment:
**Shell injection via unescaped URL/browser arguments**

Same issue as `open-group.tsx`: both `url` and `browserName` are interpolated into a shell string without escaping. Although `browserName` is constrained to dropdown values at entry time, the values are persisted to and read from `LocalStorage` as plain strings, so they are still attacker-influenced. Use `execFile` to avoid the shell entirely:

```typescript
import { execFile } from "child_process";
import { promisify } from "util";
const execFileAsync = promisify(execFile);

const openURL = async (url: string, browserName?: string) => {
  if (browserName) {
    await execFileAsync("open", ["-a", browserName, url]);
  } else {
    await execFileAsync("open", [url]);
  }
};
```

How can I resolve this? If you propose a fix, please make it concise.

};

if (newlyCreatedGroup) {
const deeplink = `raycast://extensions/Chucktaylor/fast-multilinks/open-group?arguments=${encodeURIComponent(JSON.stringify({ groupName: newlyCreatedGroup.name }))}`;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Hardcoded author slug in deeplink may not match published extension

The deeplink hardcodes Chucktaylor as the author slug. Raycast typically normalises author identifiers to lowercase when resolving extension deep-links, so raycast://extensions/Chucktaylor/... may silently fail to open the open-group command after publishing. The same pattern appears in create-group-from-clipboard.tsx line 119. Verify the exact author slug used by Raycast for this extension (usually the lowercased author field) and update both files accordingly.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/fast-multilinks/src/list-groups.tsx
Line: 106

Comment:
**Hardcoded author slug in deeplink may not match published extension**

The deeplink hardcodes `Chucktaylor` as the author slug. Raycast typically normalises author identifiers to lowercase when resolving extension deep-links, so `raycast://extensions/Chucktaylor/...` may silently fail to open the `open-group` command after publishing. The same pattern appears in `create-group-from-clipboard.tsx` line 119. Verify the exact author slug used by Raycast for this extension (usually the lowercased `author` field) and update both files accordingly.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread extensions/fast-multilinks/package.json Outdated
Comment on lines +44 to +46
"@raycast/api": "^1.104.12",
"@raycast/utils": "^1.19.1"
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Unused @raycast/utils dependency

@raycast/utils is listed as a runtime dependency but is never imported in any file under src/. Remove it to avoid unnecessary bundle bloat.

Suggested change
"@raycast/api": "^1.104.12",
"@raycast/utils": "^1.19.1"
},
"dependencies": {
"@raycast/api": "^1.104.12"
},

Rule Used: What: Every dependency listed in package.json must... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/fast-multilinks/package.json
Line: 44-46

Comment:
**Unused `@raycast/utils` dependency**

`@raycast/utils` is listed as a runtime dependency but is never imported in any file under `src/`. Remove it to avoid unnecessary bundle bloat.

```suggestion
  "dependencies": {
    "@raycast/api": "^1.104.12"
  },
```

**Rule Used:** What: Every dependency listed in package.json must... ([source](https://app.greptile.com/review/custom-context?memory=bffc60eb-f9f2-4219-b804-76e29e267d43))

How can I resolve this? If you propose a fix, please make it concise.

@raycastbot
Copy link
Copy Markdown
Collaborator

This pull request has been automatically marked as stale because it did not have any recent activity.

It will be closed if no further activity occurs in the next 7 days to keep our backlog clean 😊

@raycastbot raycastbot added the status: stalled Stalled due inactivity label May 3, 2026
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 platform: macOS status: stalled Stalled due inactivity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants