Skip to content

Add macOS Text Replacements extension#26784

Closed
jake-fowler-lego-nerd wants to merge 6 commits intoraycast:mainfrom
jake-fowler-lego-nerd:add/text-replacements
Closed

Add macOS Text Replacements extension#26784
jake-fowler-lego-nerd wants to merge 6 commits intoraycast:mainfrom
jake-fowler-lego-nerd:add/text-replacements

Conversation

@jake-fowler-lego-nerd
Copy link
Copy Markdown
Contributor

@jake-fowler-lego-nerd jake-fowler-lego-nerd commented Mar 30, 2026

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

text replacement shortcuts interface

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>
@raycastbot raycastbot added the new extension Label for PRs with new extensions label Mar 30, 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.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 30, 2026

Greptile Summary

This PR adds a new macOS Text Replacements extension that reads and writes the system SQLite database at ~/Library/KeyboardServices/TextReplacements.db, syncing changes back to System Settings via defaults import. The extension supports searching, copying, adding, editing, and deleting native macOS text replacement entries entirely from Raycast.

Key concerns to address before merge:

  • Missing CHANGELOG.md — every PR must include a changelog file at extensions/macos-text-replacements/CHANGELOG.md with an ## [Initial Release] - {PR_MERGE_DATE} entry.
  • Missing metadata/ folder — the command is a \"view\" mode command, so the extension must include Raycast-styled screenshots under extensions/macos-text-replacements/metadata/. See the Raycast docs.
  • Productivity category — the extension manages a native macOS system feature; System is the more precise category per the allowed category definitions.
  • Expansion text trimmed silentlyvalues.replacement.trim() removes intentional leading/trailing spaces from the "Expands To" field, which users sometimes deliberately include.
  • PK generation race condition — the two SELECT queries in insertReplacement run outside the transaction, creating a window where concurrent inserts could collide on the same primary key.
  • execSync with interpolated path — the osascript WAL checkpoint call uses a shell-interpolated string; replacing it with a direct execFileSync(\"/usr/bin/sqlite3\", ...) call is simpler and avoids any shell injection risk.

Confidence Score: 3/5

Not 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.

src/search-replacements.tsx (race condition, shell injection, trimming bug) and the extension root (missing CHANGELOG.md and metadata/ folder).

Important Files Changed

Filename Overview
extensions/macos-text-replacements/src/search-replacements.tsx Core logic: reads/writes macOS TextReplacements SQLite DB and syncs via defaults import; has a race condition in PK generation, execSync shell-injection risk, and silently trims expansion whitespace.
extensions/macos-text-replacements/package.json Extension manifest; Productivity category should be System for an OS-level text substitution manager; no CHANGELOG.md present in the extension folder.
extensions/macos-text-replacements/tsconfig.json Standard Raycast TypeScript config, no issues.
Prompt To Fix All 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.

---

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

Comment on lines +60 to +75
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 });
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 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.

Comment on lines +117 to +124
try {
execSync(
`osascript -e 'do shell script "/usr/bin/sqlite3 \\"${DB_PATH}\\" \\"PRAGMA wal_checkpoint(PASSIVE);\\"" '`,
{ timeout: 3000 }
);
} catch {
// non-fatal
}
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 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.

Comment on lines +143 to +144
const phrase = values.phrase.trim();
const replacement = values.replacement.trim();
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 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.

Suggested change
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.

Comment on lines +9 to +11
"categories": [
"Productivity"
],
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 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).

Suggested change
"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.

jake-fowler-lego-nerd and others added 5 commits March 30, 2026 16:31
- 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>
@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 Apr 14, 2026
@jake-fowler-lego-nerd
Copy link
Copy Markdown
Contributor Author

Bumping for review, all greptile feedback addressed in commits e824440–6156a9d.

@raycastbot raycastbot removed the status: stalled Stalled due inactivity label Apr 14, 2026
@0xdhrv 0xdhrv self-assigned this Apr 21, 2026
Copy link
Copy Markdown
Contributor

@0xdhrv 0xdhrv left a comment

Choose a reason for hiding this comment

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

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 ↗

@0xdhrv 0xdhrv marked this pull request as draft April 21, 2026 03:36
@jake-fowler-lego-nerd
Copy link
Copy Markdown
Contributor Author

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.

@0xdhrv
Copy link
Copy Markdown
Contributor

0xdhrv commented Apr 22, 2026

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.

@jake-fowler-lego-nerd
Copy link
Copy Markdown
Contributor Author

Merged into existing extension: #27349

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.

3 participants