diff --git a/changelog/2024-04-community-snippets.md b/changelog/2024-04-community-snippets.md
new file mode 100644
index 0000000000..7613c7e5c9
--- /dev/null
+++ b/changelog/2024-04-community-snippets.md
@@ -0,0 +1,6 @@
+---
+tags: [enhancement]
+pullRequest: 1998
+---
+
+- Add support for using community snippets for wrapping / cursorless insertion instead of snippets defined in Cursorless. See [Using community snippets](../docs/user/experimental/snippets.md#using-community-snippets) for more information.
diff --git a/cursorless-talon-dev/src/spoken_form_test.py b/cursorless-talon-dev/src/spoken_form_test.py
index 055df9d0ff..525f6eae74 100644
--- a/cursorless-talon-dev/src/spoken_form_test.py
+++ b/cursorless-talon-dev/src/spoken_form_test.py
@@ -33,6 +33,8 @@
 
 mockedGetValue = ""
 
+community_snippets_tag_name = "user.cursorless_use_community_snippets"
+
 
 @ctx.action_class("user")
 class UserActions:
@@ -82,6 +84,20 @@ def private_cursorless_spoken_form_test_mode(enable: bool):
                 "Cursorless spoken form tests are done. Talon microphone is re-enabled."
             )
 
+    def private_cursorless_use_community_snippets(enable: bool):
+        """Enable/disable cursorless community snippets in test mode"""
+        if enable:
+            tags = set(ctx.tags)
+            tags.add(community_snippets_tag_name)
+            ctx.tags = list(tags)
+        else:
+            tags = set(ctx.tags)
+            tags.remove(community_snippets_tag_name)
+            ctx.tags = list(tags)
+        # Note: Test harness hangs if we don't print anything because it's
+        # waiting for stdout
+        print(f"Set community snippet enablement to {enable}")
+
     def private_cursorless_spoken_form_test(
         phrase: str, mockedGetValue_: Optional[str]
     ):
diff --git a/cursorless-talon/src/cursorless.talon b/cursorless-talon/src/cursorless.talon
index f12bab8a3e..f1ab5aa206 100644
--- a/cursorless-talon/src/cursorless.talon
+++ b/cursorless-talon/src/cursorless.talon
@@ -24,15 +24,6 @@ tag: user.cursorless
 <user.cursorless_wrapper_paired_delimiter> {user.cursorless_wrap_action} <user.cursorless_target>:
     user.private_cursorless_wrap_with_paired_delimiter(cursorless_wrap_action, cursorless_target, cursorless_wrapper_paired_delimiter)
 
-{user.cursorless_insert_snippet_action} <user.cursorless_insertion_snippet>:
-    user.private_cursorless_insert_snippet(cursorless_insertion_snippet)
-
-{user.cursorless_insert_snippet_action} {user.cursorless_insertion_snippet_single_phrase} <user.text> [{user.cursorless_phrase_terminator}]:
-    user.private_cursorless_insert_snippet_with_phrase(cursorless_insertion_snippet_single_phrase, text)
-
-{user.cursorless_wrapper_snippet} {user.cursorless_wrap_action} <user.cursorless_target>:
-    user.private_cursorless_wrap_with_snippet(cursorless_wrap_action, cursorless_target, cursorless_wrapper_snippet)
-
 {user.cursorless_show_scope_visualizer} <user.cursorless_scope_type> [{user.cursorless_visualization_type}]:
     user.private_cursorless_show_scope_visualizer(cursorless_scope_type, cursorless_visualization_type or "content")
 {user.cursorless_hide_scope_visualizer}:
diff --git a/cursorless-talon/src/snippet_cursorless.talon b/cursorless-talon/src/snippet_cursorless.talon
new file mode 100644
index 0000000000..ffc28e20b4
--- /dev/null
+++ b/cursorless-talon/src/snippet_cursorless.talon
@@ -0,0 +1,14 @@
+mode: command
+mode: user.cursorless_spoken_form_test
+tag: user.cursorless
+and not tag: user.cursorless_use_community_snippets
+-
+
+{user.cursorless_insert_snippet_action} <user.cursorless_insertion_snippet>:
+    user.private_cursorless_insert_snippet(cursorless_insertion_snippet)
+
+{user.cursorless_insert_snippet_action} {user.cursorless_insertion_snippet_single_phrase} <user.text> [{user.cursorless_phrase_terminator}]:
+    user.private_cursorless_insert_snippet_with_phrase(cursorless_insertion_snippet_single_phrase, text)
+
+{user.cursorless_wrapper_snippet} {user.cursorless_wrap_action} <user.cursorless_target>:
+    user.private_cursorless_wrap_with_snippet(cursorless_wrap_action, cursorless_target, cursorless_wrapper_snippet)
diff --git a/cursorless-talon/src/snippets.py b/cursorless-talon/src/snippets.py
index 5cd9a83214..ae4b766bcb 100644
--- a/cursorless-talon/src/snippets.py
+++ b/cursorless-talon/src/snippets.py
@@ -16,6 +16,19 @@ class InsertionSnippet:
     destination: CursorlessDestination
 
 
+@dataclass
+class CommunityInsertionSnippet:
+    body: str
+    scopes: list[str] = None
+
+
+@dataclass
+class CommunityWrapperSnippet:
+    body: str
+    variable_name: str
+    scope: str = None
+
+
 mod = Module()
 
 mod.list("cursorless_insert_snippet_action", desc="Cursorless insert snippet action")
@@ -27,6 +40,11 @@ class InsertionSnippet:
     desc="tag for enabling experimental snippet support",
 )
 
+mod.tag(
+    "cursorless_use_community_snippets",
+    "If active use community snippets instead of Cursorless snippets",
+)
+
 mod.list("cursorless_wrapper_snippet", desc="Cursorless wrapper snippet")
 mod.list(
     "cursorless_insertion_snippet_no_phrase",
@@ -181,3 +199,21 @@ def cursorless_wrap_with_snippet(
             snippet_arg,
             target,
         )
+
+    def private_cursorless_insert_community_snippet(
+        name: str, destination: CursorlessDestination
+    ):
+        """Cursorless: Insert community snippet <name>"""
+        snippet: CommunityInsertionSnippet = actions.user.get_insertion_snippet(name)
+        actions.user.cursorless_insert_snippet(
+            snippet.body, destination, snippet.scopes
+        )
+
+    def private_cursorless_wrap_with_community_snippet(
+        name: str, target: CursorlessTarget
+    ):
+        """Cursorless: Wrap target with community snippet <name>"""
+        snippet: CommunityWrapperSnippet = actions.user.get_wrapper_snippet(name)
+        actions.user.cursorless_wrap_with_snippet(
+            snippet.body, target, snippet.variable_name, snippet.scope
+        )
diff --git a/cursorless-talon/src/snippets_community.talon b/cursorless-talon/src/snippets_community.talon
new file mode 100644
index 0000000000..f94e491f83
--- /dev/null
+++ b/cursorless-talon/src/snippets_community.talon
@@ -0,0 +1,13 @@
+mode: command
+mode: user.cursorless_spoken_form_test
+tag: user.cursorless
+and tag: user.cursorless_use_community_snippets
+-
+
+# These snippets are defined in community
+
+{user.cursorless_insert_snippet_action} {user.snippet} <user.cursorless_destination>:
+    user.private_cursorless_insert_community_snippet(snippet, cursorless_destination)
+
+{user.snippet_wrapper} {user.cursorless_wrap_action} <user.cursorless_target>:
+    user.private_cursorless_wrap_with_community_snippet(snippet_wrapper, cursorless_target)
diff --git a/docs/user/experimental/snippets.md b/docs/user/experimental/snippets.md
index ece8393dbb..391f03d514 100644
--- a/docs/user/experimental/snippets.md
+++ b/docs/user/experimental/snippets.md
@@ -88,6 +88,16 @@ Note that each snippet can use `insertionScopeTypes` to indicate that it will au
 | `"snippet funk"`    | Function; phrase becomes name           | Function                     | ✅                       |
 | `"snippet link"`    | Markdown link; phrase becomes link text |                              | ✅                       |
 
+## Using community snippets
+
+The community Talon files now support their own snippet format. If you'd like to use these snippets for wrapping / cursorless insertion instead of snippets defined in Cursorless, add following line to your `settings.talon` file:
+
+```talon
+tag(): user.cursorless_use_community_snippets
+```
+
+Note that this line will also disable any Cursorless snippets defined in your Cursorless customization CSVs. You will need to migrate your Cursorless snippets to the new community snippet format [described in community](https://github.com/talonhub/community/blob/main/core/snippets/README.md). If you'd be interested in a tool to help with this migration, please leave a comment on [cursorless-dev/cursorless#2149](https://github.com/cursorless-dev/cursorless/issues/2149), ideally with a link to your custom snippets for us to look at.
+
 ## Customizing spoken forms
 
 As usual, the spoken forms for these snippets can be [customized by csv](../customization.md). The csvs are in the files in `cursorless-settings/experimental` with `snippet` in their name.
diff --git a/packages/cursorless-engine/src/test/fixtures/communitySnippets.fixture.ts b/packages/cursorless-engine/src/test/fixtures/communitySnippets.fixture.ts
new file mode 100644
index 0000000000..0c4d3f55e5
--- /dev/null
+++ b/packages/cursorless-engine/src/test/fixtures/communitySnippets.fixture.ts
@@ -0,0 +1,33 @@
+import { ActionDescriptor } from "@cursorless/common";
+import { spokenFormTest } from "./spokenFormTest";
+
+const verticalRangeAction: ActionDescriptor = {
+  name: "insertSnippet",
+  destination: {
+    type: "primitive",
+    insertionMode: "after",
+    target: {
+      type: "primitive",
+      mark: {
+        character: "a",
+        symbolColor: "default",
+        type: "decoratedSymbol",
+      },
+    },
+  },
+  snippetDescription: {
+    body: "```\n$0\n```",
+    type: "custom",
+  },
+};
+
+/**
+ * These are spoken forms that have more than one way to say them, so we have to
+ * pick one in our spoken form generator, meaning we can't test the other in our
+ * Talon tests by relying on our recorded test fixtures alone.
+ */
+export const communitySnippetsSpokenFormsFixture = [
+  spokenFormTest("snippet code after air", verticalRangeAction, undefined, {
+    useCommunitySnippets: true,
+  }),
+];
diff --git a/packages/cursorless-engine/src/test/fixtures/spokenFormTest.ts b/packages/cursorless-engine/src/test/fixtures/spokenFormTest.ts
index 2422955425..0bdcf1fa10 100644
--- a/packages/cursorless-engine/src/test/fixtures/spokenFormTest.ts
+++ b/packages/cursorless-engine/src/test/fixtures/spokenFormTest.ts
@@ -23,17 +23,24 @@ export interface SpokenFormTest {
    * {@link spokenForm} is spoken.
    */
   commands: CommandV6[];
+
+  /**
+   * If `true`, use community snippets instead of Cursorless snippets
+   */
+  useCommunitySnippets: boolean;
 }
 
 export function spokenFormTest(
   spokenForm: string,
   action: ActionDescriptor,
   mockedGetValue?: unknown,
+  { useCommunitySnippets = false }: SpokenFormTestOpts = {},
 ): SpokenFormTest {
   return {
     spokenForm,
     mockedGetValue: wrapMockedGetValue(mockedGetValue),
     commands: [command(spokenForm, action)],
+    useCommunitySnippets,
   };
 }
 
@@ -41,11 +48,13 @@ export function multiActionSpokenFormTest(
   spokenForm: string,
   actions: ActionDescriptor[],
   mockedGetValue?: unknown,
+  { useCommunitySnippets = false }: SpokenFormTestOpts = {},
 ): SpokenFormTest {
   return {
     spokenForm,
     mockedGetValue: wrapMockedGetValue(mockedGetValue),
     commands: actions.map((action) => command(spokenForm, action)),
+    useCommunitySnippets,
   };
 }
 
@@ -63,3 +72,7 @@ function command(spokenForm: string, action: ActionDescriptor): CommandV6 {
     action,
   };
 }
+
+export interface SpokenFormTestOpts {
+  useCommunitySnippets?: boolean;
+}
diff --git a/packages/cursorless-engine/src/test/spokenForms.talon.test.ts b/packages/cursorless-engine/src/test/spokenForms.talon.test.ts
index 81226188b4..086300c068 100644
--- a/packages/cursorless-engine/src/test/spokenForms.talon.test.ts
+++ b/packages/cursorless-engine/src/test/spokenForms.talon.test.ts
@@ -14,6 +14,8 @@ import { TalonRepl } from "../testUtil/TalonRepl";
 import { synonymousSpokenFormsFixture } from "./fixtures/synonymousSpokenForms.fixture";
 import { talonApiFixture } from "./fixtures/talonApi.fixture";
 import { multiActionFixture } from "./fixtures/multiAction.fixture";
+import { communitySnippetsSpokenFormsFixture } from "./fixtures/communitySnippets.fixture";
+import { SpokenFormTestOpts } from "./fixtures/spokenFormTest";
 
 suite("Talon spoken forms", async function () {
   const repl = new TalonRepl();
@@ -42,8 +44,13 @@ suite("Talon spoken forms", async function () {
     ...synonymousSpokenFormsFixture,
     ...talonApiFixture,
     ...multiActionFixture,
-  ].forEach(({ spokenForm, commands, mockedGetValue }) =>
-    test(spokenForm, () => runTest(repl, spokenForm, commands, mockedGetValue)),
+    ...communitySnippetsSpokenFormsFixture,
+  ].forEach(({ spokenForm, commands, mockedGetValue, useCommunitySnippets }) =>
+    test(spokenForm, () =>
+      runTest(repl, spokenForm, commands, mockedGetValue, {
+        useCommunitySnippets,
+      }),
+    ),
   );
 });
 
@@ -76,6 +83,7 @@ async function runTest(
   spokenForm: string,
   commandsLegacy: Command[],
   mockedGetValue?: unknown,
+  { useCommunitySnippets = false }: SpokenFormTestOpts = {},
 ) {
   const commandsExpected = commandsLegacy.map((command) => ({
     ...canonicalizeAndValidateCommand(command),
@@ -101,24 +109,37 @@ async function runTest(
       ? "None"
       : JSON.stringify(JSON.stringify(mockedGetValue));
 
-  const result = await repl.action(
-    `user.private_cursorless_spoken_form_test("${spokenForm}", ${mockedGetValueString})`,
-  );
+  if (useCommunitySnippets) {
+    await repl.action(`user.private_cursorless_use_community_snippets(True)`);
+  }
 
-  const commandsActual = (() => {
-    try {
-      return JSON.parse(result);
-    } catch (e) {
-      throw Error(result);
+  try {
+    const result = await repl.action(
+      `user.private_cursorless_spoken_form_test("${spokenForm}", ${mockedGetValueString})`,
+    );
+
+    const commandsActual = (() => {
+      try {
+        return JSON.parse(result);
+      } catch (e) {
+        throw Error(result);
+      }
+    })();
+
+    assert.deepStrictEqual(commandsActual, commandsExpected);
+  } finally {
+    if (useCommunitySnippets) {
+      await repl.action(
+        `user.private_cursorless_use_community_snippets(False)`,
+      );
     }
-  })();
-
-  assert.deepStrictEqual(commandsActual, commandsExpected);
+  }
 }
 
 async function setTestMode(repl: TalonRepl, enabled: boolean) {
   const arg = enabled ? "True" : "False";
   await repl.action(`user.private_cursorless_spoken_form_test_mode(${arg})`);
+  await repl.action(`user.private_cursorless_use_community_snippets(False)`);
 
   // If you have warnings in your talon user files, they will be printed to the
   // repl when you run the above action. We need to eat them so that they don't