diff --git a/editor-extensions/vscode/package.json b/editor-extensions/vscode/package.json
index 2115139..1806ae6 100644
--- a/editor-extensions/vscode/package.json
+++ b/editor-extensions/vscode/package.json
@@ -28,10 +28,24 @@
   ],
   "main": "./out/extension",
   "contributes": {
+    "menus": {
+      "editor/title/run": [
+        {
+          "command": "grain.runEditorFile",
+          "when": "resourceLangId == grain",
+          "group": "navigation@1"
+        }
+      ]
+    },
     "commands": [
       {
         "command": "grain.restart",
         "title": "Grain: Restart Language Server"
+      },
+      {
+        "command": "grain.runEditorFile",
+        "title": "Grain: Run File",
+        "icon": "$(play)"
       }
     ],
     "languages": [
@@ -89,6 +103,12 @@
           "type": "string",
           "description": "Absolute path to the grain CLI (detected in PATH if not specified)"
         },
+        "grain.runCommandLayout": {
+          "scope": "resource",
+          "type": "string",
+          "default": "${cliPath} ${activeFile} ${cliFlags}",
+          "description": "The layout of the run command"
+        },
         "grain.enableLSP": {
           "scope": "resource",
           "type": "boolean",
diff --git a/editor-extensions/vscode/src/extension.ts b/editor-extensions/vscode/src/extension.ts
index 72b24e6..a21c369 100644
--- a/editor-extensions/vscode/src/extension.ts
+++ b/editor-extensions/vscode/src/extension.ts
@@ -138,6 +138,21 @@ function getLspCommand(uri: Uri) {
   return [command, args] as const;
 }
 
+function getRunCommand(uri: Uri) {
+  let config = workspace.getConfiguration("grain", uri);
+
+  let command = config.get<string | undefined>("cliPath") || findGrain();
+  // For some reason, if you specify a capitalized EXE extension for our pkg binary,
+  // it crashes the LSP so we just lowercase any .EXE ending in the command
+  command = command.replace(/\.EXE$/, ".exe");
+
+  let flags = config.get<string | undefined>("cliFlags") || "";
+
+  let args = flags.split(" ");
+
+  return [command, args] as const;
+}
+
 async function startFileClient(uri: Uri) {
   let [command, args] = getLspCommand(uri);
 
@@ -268,6 +283,34 @@ async function restartAllClients() {
     await client.restart();
   }
 }
+async function runEditorFile(resource: Uri) {
+  let targetResource = resource;
+  if (!targetResource && window.activeTextEditor) {
+    targetResource = window.activeTextEditor.document.uri;
+  }
+  if (targetResource) {
+    const [command, args] = getRunCommand(targetResource);
+
+    const config = workspace.getConfiguration();
+    const grainRunCommand = config
+      .get<string>(
+        "grain.runCommandLayout",
+        "${cliPath} ${activeFile} ${cliFlags}"
+      )
+      .replaceAll("${cliPath}", command)
+      .replaceAll("${activeFile}", targetResource.fsPath)
+      .replaceAll("${cliFlags}", args.join(" "));
+
+    // Create a new terminal instance
+    const terminal = window.createTerminal("Grain");
+
+    // Send a command to the terminal
+    terminal.sendText(grainRunCommand);
+
+    // Show the terminal
+    terminal.show();
+  }
+}
 
 async function didOpenTextDocument(
   document: TextDocument
@@ -359,11 +402,13 @@ export async function activate(context: ExtensionContext): Promise<void> {
     didChangeWorkspaceFolders
   );
   let restart$ = commands.registerCommand("grain.restart", restartAllClients);
+  const run$ = commands.registerCommand("grain.runEditorFile", runEditorFile);
 
   context.subscriptions.push(
     didOpenTextDocument$,
     didChangeWorkspaceFolders$,
-    restart$
+    restart$,
+    run$
   );
 
   for (let doc of workspace.textDocuments) {