You type the same things over and over. Your email signature. A mailing address. The boilerplate at the top of every invoice. A Zoom link you paste into three calendar invites a day. A text expander replaces those repeated strings with a short trigger you type once and never think about again. Type ;addr, get your full address. Type ;sig, get your signature. The classic version of this job is exactly what TextExpander built a business on, and it's worth doing well.
The catch with the big-name expanders is where your library lives. Your snippets - which can include addresses, account numbers, internal URLs, code you reuse - sit in a vendor's cloud account, synced through their servers, behind a recurring subscription. ToolPiper does the same expansion entirely on your Mac. Your triggers and their expansions are stored locally, there's no monthly charge for text replacement, and nothing syncs to a server you can't inspect. That's the trade we're making, and there's a real cost to it (no cross-device sync) that we'll get to.
This article is about static and dynamic expansion - the deterministic, no-AI core. If you want the next step up, where a trigger like ;fix sends your selected text to a local model and rewrites it, that's a separate feature covered in AI text snippets on Mac. Here we're focused on the part that's been a solved problem for fifteen years, done locally.
What is text expansion and why would a power user want it?
Text expansion is a system that watches what you type and replaces a short trigger string with a longer, predefined block of text. Power users want it because it removes the repetitive typing that fills a workday: signatures, addresses, code boilerplate, canned replies, and date-stamped filenames.
The mechanism is simple. A small background process monitors keystrokes for trigger strings you've defined. When you type one, it deletes the trigger characters and types the expansion in their place. The win compounds: a snippet you use thirty times a day saves more keystrokes per month than you'd expect, and it removes the small errors that creep in when you retype the same address for the hundredth time.
The reason this matters more for power users than casual ones is volume and consistency. Support reps send the same answers. Lawyers paste the same clauses. Developers retype the same import blocks and license headers. Once your most-typed text becomes a two-keystroke trigger, you stop context-switching to hunt for the canonical version of a thing - the trigger is the canonical version.
Why run a text expander locally instead of in the cloud?
A local text expander keeps your snippet library on your Mac instead of in a vendor's cloud account, which means no per-use or subscription cost for text replacement and no copy of your private snippets sitting on a server you can't audit. The trade-off is that local-only snippets don't sync to your other devices.
Text replacement is mechanically trivial - it does not need a server. The cloud in a cloud expander exists almost entirely to sync your library across devices and to support team-shared libraries, both real features. But syncing means your snippets, which often contain addresses, internal links, account numbers, and reused code, are stored and transmitted by a third party. For a tool whose entire job is inserting text into other apps, that's worth thinking about.
The cost angle is straightforward. TextExpander is a subscription, around $3.33 per month billed annually as of May 2026, roughly $40 a year for the ability to type shortcuts. ToolPiper ships text expansion as part of a free download. You'll find more on that comparison in our TextExpander alternative for Mac writeup. The privacy angle is the same one we apply across local-first AI on macOS: the work happens on your machine, so there's no transit to audit in the first place.
How does the typed-trigger engine work?
ToolPiper watches for a delimiter plus an abbreviation (for example, ;addr) and expands it the moment the trigger completes. When two triggers overlap, it matches the longest one first, so a more specific trigger always wins over a shorter prefix.
The matching is whitespace and punctuation-flanked, which is the detail that keeps expansion from misfiring. A trigger like ;fix only fires as its own token - it will never expand inside the word ;fixed, because the characters around it aren't a boundary. That sounds like a small thing until you've used an expander that interrupts you mid-word.
Longest-trigger-first matching is the other piece that makes a real library usable. Say you have both ;ad (your ad copy) and ;addr (your address). When you finish typing ;addr, the engine resolves it to the address, not to the ad copy plus a stray dr. You build your shortcuts the way they read in your head, and the engine sorts out the overlaps.
What dynamic values can a snippet resolve?
ToolPiper resolves 11 dynamic values at expand time - things like the current date, time, your name, your computer's hostname, and the frontmost app. Each one computes fresh every time the snippet fires, so ;today always inserts today's date, not the date you created the snippet.
Static text is half the job. The other half is values that change. A filename that needs today's date. A log line that wants an ISO 8601 timestamp. An email greeting that should address you by your account name. These resolve when you trigger the snippet, not when you write it. Here's the full set:
| Trigger | Resolves to |
|---|---|
;today | Today's date, long format (for example, May 18, 2026) |
;date | Today's date, short format |
;time | The current time |
;now | The current date and time together |
;iso | The date in ISO 8601 form |
;timestamp | A full ISO 8601 timestamp |
;name | Your full user name |
;login | Your short (login) user name |
;myemail | The email on your ToolPiper license |
;host | Your computer's name |
;app | The frontmost app at the moment you expand |
These compose with static snippets. A bug-report template can open with ;timestamp on ;host in ;app and produce a fully stamped header without you typing a single field. A signature can pull ;name and ;myemail so it stays correct if you change your account details. The values are computed by macOS at expand time, so there's nothing to keep in sync.
How do you control where the cursor lands and how a trigger matches?
Place the marker %| anywhere in a snippet and your cursor lands there after expansion, so template snippets drop you straight into the field you need to fill. Case-sensitivity modes give you finer control over how each trigger matches the text you type.
Cursor placement is the feature that turns a snippet from a paste into a template. Define an email reply as Hi %|, followed by your standard body, and after expansion the cursor sits right after "Hi " waiting for the name. No arrow-keying back into position. One marker, one snippet.
Two controls shape behavior beyond the raw text:
- Case-sensitivity modes. Each snippet can be set to sensitive (the trigger must match exactly), ignore (case doesn't matter), or adapt (the expansion follows the case you typed the trigger in). Adapt is the one you want for snippets that start sentences.
- Longest-first plus boundary matching. Covered above - together they're what keeps a large library from stepping on itself.
App-aware suppression, where a trigger can be scoped so it only fires in the apps you choose rather than globally in every text field, is in beta and rolling out separately. Until it lands, triggers fire in any standard text field where injection works.
Can you import your snippets from another expander?
Yes. ToolPiper imports from TextExpander (.textexpander files), Raycast (JSON export), and generic CSV, so your existing library moves over without retyping every trigger.
Switching tools is only worth it if your library comes with you. If you've spent years building a TextExpander group, export it and import the .textexpander file directly. Coming from Raycast snippets, export to JSON and import that. And if your shortcuts live in a spreadsheet or some other tool that can dump a CSV, that path works too - one trigger and one expansion per row.
Static triggers, expansion text, and cursor markers carry over. Dynamic-value triggers are ToolPiper's own syntax, so a snippet that needs ;today is something you'd add or adjust after the import. In practice the bulk of a real library is static text, and that's the part that transfers cleanly.
Where does AI fit in?
The static and dynamic engine described above uses no AI at all - a trigger maps to fixed text or a computed value. AI lives in a separate feature called action snippets: select text, tap the right Command key, and a trigger like ;fix rewrites the selection through an on-device model, falling back to a local LLM. Classic expansion works fully without it.
Everything above is deterministic. A trigger maps to fixed text or a computed value, and that's it - no model, no network, nothing to wait for. That predictability is exactly what you want from the core of an expander.
The step up is action snippets. Select a paragraph, tap the right Command key, and a trigger like ;fix or ;formal rewrites the selection through a model running on your Mac. Same muscle memory, but now the expansion is a transform instead of a constant. That's a different enough feature that it has its own article: AI text snippets on Mac. If you only want classic expansion, you never have to touch it - the static and dynamic engine stands on its own.
Where text expansion falls short
Injection isn't universal. ToolPiper expands in standard macOS text fields, but some apps render and handle text in non-standard ways - certain terminal emulators, some Electron apps, and editors with custom text rendering may not accept injected expansion. When that happens the trigger simply doesn't fire in that field. It's a per-app limitation, not a crash, and every other app on your Mac keeps working normally.
There's no cloud sync across your devices, and that's by design. Your snippet library lives on the Mac it was created on. If you work across two Macs and want the same library on both, a local-only expander makes that harder than a synced one does. This is the clearest place where TextExpander is genuinely better: cloud sync and team-shared libraries are real strengths, and a cross-platform tool that keeps your snippets identical on Mac, Windows, iOS, and web is doing something we don't match. If shared libraries across a team or constant device-hopping is your daily reality, that's a fair reason to pay for a synced product. We chose local, and the cost of that choice is sync.
Text expansion on Mac: how the options compare
The comparison table below lays the field out plainly. A few notes on reading it: Espanso is free and open-source and runs everywhere, but you configure it by editing YAML files rather than a GUI. Raycast bundles snippets into a full launcher and window manager, so if you want all of that in one place it's a strong pick. Typinator is a long-standing one-time-purchase Mac expander. The honest summary is that each tool optimizes for something different. Pick by what you need most: cross-device sync, scripting, a built-in launcher, or a local GUI with no subscription.
Get ToolPiper
ToolPiper is a free download from modelpiper.com (DMG). It requires macOS 26 or later and Apple Silicon (M1 or newer). It isn't on the Mac App Store because text expansion needs Accessibility and CGEvent APIs that the App Store sandbox doesn't allow - the same reason TextExpander, Raycast, Alfred, and Keyboard Maestro ship outside it. Grant Accessibility once on first launch and your static and dynamic snippets work immediately.
This is part of a series on text and clipboard workflows on Mac and the broader pillar on local-first AI on macOS. Next up: AI text snippets on Mac - the same trigger muscle memory, but the expansion is a local model rewrite of your selection.
