Add macOS Text Replacements extension#26784
Add macOS Text Replacements extension#26784jake-fowler-lego-nerd wants to merge 6 commits intoraycast:mainfrom
Conversation
Search, copy, add, edit, and delete native macOS text replacements directly from Raycast. Reads and writes the system SQLite database, syncing changes back to System Settings via defaults import. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
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. |
Greptile SummaryThis PR adds a new macOS Text Replacements extension that reads and writes the system SQLite database at Key concerns to address before merge:
Confidence Score: 3/5Not safe to merge yet — missing required CHANGELOG.md and metadata/ screenshots, plus a few code-quality issues in the main source file. Two required store-publishing artifacts are absent (CHANGELOG.md and metadata/ folder), which are mandatory for every new Raycast extension. The main source file also has a silent data-integrity issue (expansion text trimming) and a race condition in PK generation. These together push the score below 4.
Important Files Changed
Prompt To Fix All With AIThis is a comment left during a code review.
Path: extensions/macos-text-replacements/src/search-replacements.tsx
Line: 60-75
Comment:
**Race condition in primary key generation**
The two `SELECT` queries (`seqMax` and `tableMax`) are executed outside the transaction, creating a TOCTOU (time-of-check/time-of-use) window. If two inserts are triggered in quick succession (e.g., duplicate form submission, background sync by iCloud), both could observe the same maximum PK and then attempt to insert with the same `nextPK`, causing a `UNIQUE constraint failed` error and rolling back the second insert silently.
Moving the max reads inside the `BEGIN … COMMIT` block (and using `MAX()` directly in the `INSERT` via a subquery) would make the operation fully atomic:
```
INSERT INTO ZTEXTREPLACEMENTENTRY (Z_PK, Z_ENT, Z_OPT, ZNEEDSSAVETOCLOUD, ZWASDELETED, ZTIMESTAMP, ZPHRASE, ZSHORTCUT, ZUNIQUENAME)
SELECT MAX(Z_PK)+1, 1, 1, 1, 0, <timestamp>, <phrase>, <shortcut>, <unique>
FROM ZTEXTREPLACEMENTENTRY;
```
(Then update `Z_PRIMARYKEY` using the same subquery expression.)
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/macos-text-replacements/src/search-replacements.tsx
Line: 117-124
Comment:
**Shell-injection risk via `execSync` with interpolated path**
`execSync` passes the command to the shell (`/bin/sh`), so any shell metacharacters in `DB_PATH` (e.g., a space in the home directory path on some managed Macs) would break the command or — in adversarial situations — allow command injection. `DB_PATH` is derived from `homedir()`, which is user-controlled on some macOS configurations.
Also, a WAL checkpoint can be triggered directly without going through `osascript`:
```typescript
try {
execFileSync("/usr/bin/sqlite3", [DB_PATH, "PRAGMA wal_checkpoint(PASSIVE);"], { timeout: 3000 });
} catch {
// non-fatal
}
```
This avoids the shell entirely and is simpler and safer.
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/macos-text-replacements/src/search-replacements.tsx
Line: 143-144
Comment:
**`trim()` silently strips intentional whitespace from the expansion text**
Calling `.trim()` on `values.replacement` removes leading and trailing spaces from the "Expands To" value. Users who deliberately want an expansion to begin or end with a space (e.g., `" and "`) will silently lose those characters. Only `phrase` (the shortcut) needs trimming; the replacement should be kept as-is.
```suggestion
const phrase = values.phrase.trim();
const replacement = values.replacement;
```
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/macos-text-replacements/package.json
Line: 9-11
Comment:
**Category should be `System`, not `Productivity`**
This extension manages a native macOS OS-level feature (system text replacements). Per the category guidelines, **System** is defined as "OS-level controls and system behavior" — the canonical example is Coffee, which keeps the Mac awake. Managing macOS keyboard/text substitutions fits that definition more precisely than `Productivity` (task management and personal productivity tools, e.g. Todoist).
```suggestion
"categories": [
"System"
],
```
**Rule Used:** What: Assign at least one predefined category to e... ([source](https://app.greptile.com/review/custom-context?memory=f49debbf-b6f6-4c0d-9b35-e1927815992b))
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "Add macOS Text Replacements extension" | Re-trigger Greptile |
| const seqMax = parseInt(query("SELECT Z_MAX FROM Z_PRIMARYKEY WHERE Z_ENT=1;").trim()) || 0; | ||
| const tableMax = parseInt(query("SELECT MAX(Z_PK) FROM ZTEXTREPLACEMENTENTRY;").trim()) || 0; | ||
| const nextPK = Math.max(seqMax, tableMax) + 1; | ||
| const uniqueName = `${Date.now()}-${Math.random().toString(36).slice(2)}`; | ||
| // CoreData timestamps are seconds since 2001-01-01, not Unix epoch | ||
| const timestamp = Date.now() / 1000 - 978307200; | ||
|
|
||
| const script = ` | ||
| BEGIN; | ||
| INSERT INTO ZTEXTREPLACEMENTENTRY | ||
| (Z_PK, Z_ENT, Z_OPT, ZNEEDSSAVETOCLOUD, ZWASDELETED, ZTIMESTAMP, ZPHRASE, ZSHORTCUT, ZUNIQUENAME) | ||
| VALUES (${nextPK}, 1, 1, 1, 0, ${timestamp}, ${sql(replacement)}, ${sql(phrase)}, ${sql(uniqueName)}); | ||
| UPDATE Z_PRIMARYKEY SET Z_MAX=${nextPK} WHERE Z_ENT=1; | ||
| COMMIT;`; | ||
|
|
||
| execFileSync("/usr/bin/sqlite3", [DB_PATH], { input: script, encoding: "utf8", timeout: 5000 }); |
There was a problem hiding this comment.
Race condition in primary key generation
The two SELECT queries (seqMax and tableMax) are executed outside the transaction, creating a TOCTOU (time-of-check/time-of-use) window. If two inserts are triggered in quick succession (e.g., duplicate form submission, background sync by iCloud), both could observe the same maximum PK and then attempt to insert with the same nextPK, causing a UNIQUE constraint failed error and rolling back the second insert silently.
Moving the max reads inside the BEGIN … COMMIT block (and using MAX() directly in the INSERT via a subquery) would make the operation fully atomic:
INSERT INTO ZTEXTREPLACEMENTENTRY (Z_PK, Z_ENT, Z_OPT, ZNEEDSSAVETOCLOUD, ZWASDELETED, ZTIMESTAMP, ZPHRASE, ZSHORTCUT, ZUNIQUENAME)
SELECT MAX(Z_PK)+1, 1, 1, 1, 0, <timestamp>, <phrase>, <shortcut>, <unique>
FROM ZTEXTREPLACEMENTENTRY;
(Then update Z_PRIMARYKEY using the same subquery expression.)
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/macos-text-replacements/src/search-replacements.tsx
Line: 60-75
Comment:
**Race condition in primary key generation**
The two `SELECT` queries (`seqMax` and `tableMax`) are executed outside the transaction, creating a TOCTOU (time-of-check/time-of-use) window. If two inserts are triggered in quick succession (e.g., duplicate form submission, background sync by iCloud), both could observe the same maximum PK and then attempt to insert with the same `nextPK`, causing a `UNIQUE constraint failed` error and rolling back the second insert silently.
Moving the max reads inside the `BEGIN … COMMIT` block (and using `MAX()` directly in the `INSERT` via a subquery) would make the operation fully atomic:
```
INSERT INTO ZTEXTREPLACEMENTENTRY (Z_PK, Z_ENT, Z_OPT, ZNEEDSSAVETOCLOUD, ZWASDELETED, ZTIMESTAMP, ZPHRASE, ZSHORTCUT, ZUNIQUENAME)
SELECT MAX(Z_PK)+1, 1, 1, 1, 0, <timestamp>, <phrase>, <shortcut>, <unique>
FROM ZTEXTREPLACEMENTENTRY;
```
(Then update `Z_PRIMARYKEY` using the same subquery expression.)
How can I resolve this? If you propose a fix, please make it concise.| try { | ||
| execSync( | ||
| `osascript -e 'do shell script "/usr/bin/sqlite3 \\"${DB_PATH}\\" \\"PRAGMA wal_checkpoint(PASSIVE);\\"" '`, | ||
| { timeout: 3000 } | ||
| ); | ||
| } catch { | ||
| // non-fatal | ||
| } |
There was a problem hiding this comment.
Shell-injection risk via
execSync with interpolated path
execSync passes the command to the shell (/bin/sh), so any shell metacharacters in DB_PATH (e.g., a space in the home directory path on some managed Macs) would break the command or — in adversarial situations — allow command injection. DB_PATH is derived from homedir(), which is user-controlled on some macOS configurations.
Also, a WAL checkpoint can be triggered directly without going through osascript:
try {
execFileSync("/usr/bin/sqlite3", [DB_PATH, "PRAGMA wal_checkpoint(PASSIVE);"], { timeout: 3000 });
} catch {
// non-fatal
}This avoids the shell entirely and is simpler and safer.
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/macos-text-replacements/src/search-replacements.tsx
Line: 117-124
Comment:
**Shell-injection risk via `execSync` with interpolated path**
`execSync` passes the command to the shell (`/bin/sh`), so any shell metacharacters in `DB_PATH` (e.g., a space in the home directory path on some managed Macs) would break the command or — in adversarial situations — allow command injection. `DB_PATH` is derived from `homedir()`, which is user-controlled on some macOS configurations.
Also, a WAL checkpoint can be triggered directly without going through `osascript`:
```typescript
try {
execFileSync("/usr/bin/sqlite3", [DB_PATH, "PRAGMA wal_checkpoint(PASSIVE);"], { timeout: 3000 });
} catch {
// non-fatal
}
```
This avoids the shell entirely and is simpler and safer.
How can I resolve this? If you propose a fix, please make it concise.| const phrase = values.phrase.trim(); | ||
| const replacement = values.replacement.trim(); |
There was a problem hiding this comment.
trim() silently strips intentional whitespace from the expansion text
Calling .trim() on values.replacement removes leading and trailing spaces from the "Expands To" value. Users who deliberately want an expansion to begin or end with a space (e.g., " and ") will silently lose those characters. Only phrase (the shortcut) needs trimming; the replacement should be kept as-is.
| const phrase = values.phrase.trim(); | |
| const replacement = values.replacement.trim(); | |
| const phrase = values.phrase.trim(); | |
| const replacement = values.replacement; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/macos-text-replacements/src/search-replacements.tsx
Line: 143-144
Comment:
**`trim()` silently strips intentional whitespace from the expansion text**
Calling `.trim()` on `values.replacement` removes leading and trailing spaces from the "Expands To" value. Users who deliberately want an expansion to begin or end with a space (e.g., `" and "`) will silently lose those characters. Only `phrase` (the shortcut) needs trimming; the replacement should be kept as-is.
```suggestion
const phrase = values.phrase.trim();
const replacement = values.replacement;
```
How can I resolve this? If you propose a fix, please make it concise.| "categories": [ | ||
| "Productivity" | ||
| ], |
There was a problem hiding this comment.
Category should be
System, not Productivity
This extension manages a native macOS OS-level feature (system text replacements). Per the category guidelines, System is defined as "OS-level controls and system behavior" — the canonical example is Coffee, which keeps the Mac awake. Managing macOS keyboard/text substitutions fits that definition more precisely than Productivity (task management and personal productivity tools, e.g. Todoist).
| "categories": [ | |
| "Productivity" | |
| ], | |
| "categories": [ | |
| "System" | |
| ], |
Rule Used: What: Assign at least one predefined category to e... (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/macos-text-replacements/package.json
Line: 9-11
Comment:
**Category should be `System`, not `Productivity`**
This extension manages a native macOS OS-level feature (system text replacements). Per the category guidelines, **System** is defined as "OS-level controls and system behavior" — the canonical example is Coffee, which keeps the Mac awake. Managing macOS keyboard/text substitutions fits that definition more precisely than `Productivity` (task management and personal productivity tools, e.g. Todoist).
```suggestion
"categories": [
"System"
],
```
**Rule Used:** What: Assign at least one predefined category to e... ([source](https://app.greptile.com/review/custom-context?memory=f49debbf-b6f6-4c0d-9b35-e1927815992b))
How can I resolve this? If you propose a fix, please make it concise.- Add CHANGELOG.md - Add metadata screenshot - Fix category to System - Fix author to match Raycast account Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move icon to assets/icon.png at 512x512 - Resize screenshot to required 2000x1250px Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- insertReplacement: compute and use PK inside a single BEGIN IMMEDIATE transaction to eliminate TOCTOU race - notifySystem: replace execSync+osascript with execFileSync for WAL checkpoint, removing any shell injection surface - handleSubmit: stop trimming replacement text to preserve intentional whitespace Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
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 😊 |
|
Bumping for review, all greptile feedback addressed in commits e824440–6156a9d. |
0xdhrv
left a comment
There was a problem hiding this comment.
Thanks for your contribution {cursor} 🔥
We already have an extension in the Store that deals with this. Could we consider enhancing the existing extension below instead of creating another one?
If there are unique features or workflows you’re aiming to add, we’d love to hear them and see if they can be integrated into this to avoid duplication and improve discoverability.
This would help avoid duplication and keep related functionality consolidated in one place.
As mentioned in our extension guidelines here ↗
I saw that extension, but mine is different in that it doesn't import into the Raycast snippets, it leaves them in the Test Replacements functionality in MacOS and allows for better access to them. I like using Apples text replacements because they are native and sync cleanly between Apple devices. The interface to manage them is a little too hidden in my opinion. If that could be added to the existing extension as an option, I'm not opposed to the idea. Thanks for the consideration. |
Yeah, this is what I mean. The existing extension can be extended to support the features you want to add. You can fork the extension, add those features, and we’ll review it soon. |
|
Merged into existing extension: #27349 |
Description
The native text replacements functionality is pretty robust but the interface is buried in System Settings. With this extension you can search, copy, add, edit, and delete native macOS text replacements from Raycast.
Reads and writes the system SQLite database at ~/Library/KeyboardServices/TextReplacements.db, syncing changes back to System Settings automatically.
Screencast