diff --git a/crates/project-model/src/lib.rs b/crates/project-model/src/lib.rs
index 5b91f5d80589..e464b6a3537f 100644
--- a/crates/project-model/src/lib.rs
+++ b/crates/project-model/src/lib.rs
@@ -21,7 +21,7 @@ mod build_scripts;
 mod cargo_workspace;
 mod cfg_flag;
 mod manifest_path;
-mod project_json;
+pub mod project_json;
 mod rustc_cfg;
 mod sysroot;
 pub mod target_data_layout;
diff --git a/crates/project-model/src/project_json.rs b/crates/project-model/src/project_json.rs
index fba0aaa8ce9f..867f9705dfa1 100644
--- a/crates/project-model/src/project_json.rs
+++ b/crates/project-model/src/project_json.rs
@@ -56,7 +56,7 @@ use rustc_hash::FxHashMap;
 use serde::{de, Deserialize};
 use std::path::PathBuf;
 
-use crate::cfg_flag::CfgFlag;
+use crate::{cfg_flag::CfgFlag, TargetKind};
 
 /// Roots and crates that compose this Rust project.
 #[derive(Clone, Debug, Eq, PartialEq)]
@@ -87,6 +87,21 @@ pub struct Crate {
     pub(crate) exclude: Vec<AbsPathBuf>,
     pub(crate) is_proc_macro: bool,
     pub(crate) repository: Option<String>,
+    pub build_info: Option<BuildInfo>,
+}
+
+/// Additional metadata about a crate, used to configure runnables.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct BuildInfo {
+    /// The name associated with this crate, according to the custom
+    /// build system being used.
+    pub label: String,
+    /// What kind of target is this crate? For example, we don't want
+    /// to offer a 'run' button for library crates.
+    pub target_kind: TargetKind,
+    /// Configuration for shell commands, such as CLI invocations for
+    /// a check build or a test run.
+    pub shell_runnables: Vec<ShellRunnableArgs>,
 }
 
 impl ProjectJson {
@@ -121,6 +136,15 @@ impl ProjectJson {
                         None => (vec![root_module.parent().unwrap().to_path_buf()], Vec::new()),
                     };
 
+                    let build_info = match crate_data.build_info {
+                        Some(build_info) => Some(BuildInfo {
+                            label: build_info.label,
+                            target_kind: build_info.target_kind.into(),
+                            shell_runnables: build_info.shell_runnables,
+                        }),
+                        None => None,
+                    };
+
                     Crate {
                         display_name: crate_data
                             .display_name
@@ -149,6 +173,7 @@ impl ProjectJson {
                         exclude,
                         is_proc_macro: crate_data.is_proc_macro,
                         repository: crate_data.repository,
+                        build_info,
                     }
                 })
                 .collect(),
@@ -172,6 +197,14 @@ impl ProjectJson {
     pub fn path(&self) -> &AbsPath {
         &self.project_root
     }
+
+    pub fn crate_by_root(&self, root: &AbsPath) -> Option<Crate> {
+        self.crates
+            .iter()
+            .filter(|krate| krate.is_workspace_member)
+            .find(|krate| &krate.root_module == root)
+            .cloned()
+    }
 }
 
 #[derive(Deserialize, Debug, Clone)]
@@ -201,6 +234,8 @@ struct CrateData {
     is_proc_macro: bool,
     #[serde(default)]
     repository: Option<String>,
+    #[serde(default)]
+    build_info: Option<BuildInfoData>,
 }
 
 #[derive(Deserialize, Debug, Clone)]
@@ -216,6 +251,48 @@ enum EditionData {
     Edition2024,
 }
 
+#[derive(Deserialize, Debug, Clone)]
+pub struct BuildInfoData {
+    label: String,
+    target_kind: TargetKindData,
+    shell_runnables: Vec<ShellRunnableArgs>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ShellRunnableArgs {
+    pub program: String,
+    pub args: Vec<String>,
+    pub cwd: PathBuf,
+    pub kind: ShellRunnableKind,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub enum ShellRunnableKind {
+    Check,
+    Run,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub enum TargetKindData {
+    Bin,
+    /// Any kind of Cargo lib crate-type (dylib, rlib, proc-macro, ...).
+    Lib,
+    Test,
+}
+
+impl From<TargetKindData> for TargetKind {
+    fn from(value: TargetKindData) -> Self {
+        match value {
+            TargetKindData::Bin => TargetKind::Bin,
+            TargetKindData::Lib => TargetKind::Lib { is_proc_macro: false },
+            TargetKindData::Test => TargetKind::Test,
+        }
+    }
+}
+
 impl From<EditionData> for Edition {
     fn from(data: EditionData) -> Self {
         match data {
diff --git a/crates/rust-analyzer/src/global_state.rs b/crates/rust-analyzer/src/global_state.rs
index 064dc21fe2f8..f8f912431ef0 100644
--- a/crates/rust-analyzer/src/global_state.rs
+++ b/crates/rust-analyzer/src/global_state.rs
@@ -33,7 +33,7 @@ use crate::{
     mem_docs::MemDocs,
     op_queue::OpQueue,
     reload,
-    target_spec::{CargoTargetSpec, TargetSpec},
+    target_spec::{CargoTargetSpec, TargetSpec, ProjectJsonTargetSpec},
     task_pool::{TaskPool, TaskQueue},
 };
 
@@ -527,7 +527,20 @@ impl GlobalStateSnapshot {
                         features: package_data.features.keys().cloned().collect(),
                     }));
                 }
-                ProjectWorkspace::Json { .. } => {}
+                ProjectWorkspace::Json { project, .. } => {
+                    let Some(krate) = project.crate_by_root(path) else {
+                        continue;
+                    };
+                    let Some(build_info) = krate.build_info else {
+                        continue;
+                    };
+
+                    return Some(TargetSpec::ProjectJson(ProjectJsonTargetSpec {
+                        target_kind: build_info.target_kind,
+                        label: build_info.label,
+                        shell_runnables: build_info.shell_runnables,
+                    }));
+                }
                 ProjectWorkspace::DetachedFiles { .. } => {}
             }
         }
diff --git a/crates/rust-analyzer/src/handlers/request.rs b/crates/rust-analyzer/src/handlers/request.rs
index 96f0c41ab651..f1c7fc25f1da 100644
--- a/crates/rust-analyzer/src/handlers/request.rs
+++ b/crates/rust-analyzer/src/handlers/request.rs
@@ -773,7 +773,7 @@ pub(crate) fn handle_parent_module(
         };
         let cargo_spec = match TargetSpec::for_file(&snap, file_id)? {
             Some(TargetSpec::Cargo(it)) => it,
-            None => return Ok(None),
+            Some(TargetSpec::ProjectJson(_)) | None => return Ok(None),
         };
 
         if snap.analysis.crate_root(crate_id)? == file_id {
@@ -826,7 +826,6 @@ pub(crate) fn handle_runnables(
         }
         if let Some(mut runnable) = to_proto::runnable(&snap, runnable)? {
             if expect_test {
-                #[allow(irrefutable_let_patterns)]
                 if let lsp_ext::RunnableArgs::Cargo(r) = &mut runnable.args {
                     runnable.label = format!("{} + expect", runnable.label);
                     r.expect_test = Some(true);
@@ -866,6 +865,7 @@ pub(crate) fn handle_runnables(
                 })
             }
         }
+        Some(TargetSpec::ProjectJson(_)) => {}
         None => {
             if !snap.config.linked_or_discovered_projects().is_empty() {
                 res.push(lsp_ext::Runnable {
@@ -1770,7 +1770,7 @@ pub(crate) fn handle_open_cargo_toml(
 
     let cargo_spec = match TargetSpec::for_file(&snap, file_id)? {
         Some(TargetSpec::Cargo(it)) => it,
-        None => return Ok(None),
+        Some(TargetSpec::ProjectJson(_)) | None => return Ok(None),
     };
 
     let cargo_toml_url = to_proto::url_from_abs_path(&cargo_spec.cargo_toml);
@@ -2062,7 +2062,7 @@ fn run_rustfmt(
                     };
                     process::Command::new(cmd_path)
                 }
-                None => process::Command::new(cmd),
+                _ => process::Command::new(cmd),
             };
 
             cmd.envs(snap.config.extra_env());
diff --git a/crates/rust-analyzer/src/lsp/ext.rs b/crates/rust-analyzer/src/lsp/ext.rs
index fe4a5382e7f1..fa5b041a7ce5 100644
--- a/crates/rust-analyzer/src/lsp/ext.rs
+++ b/crates/rust-analyzer/src/lsp/ext.rs
@@ -431,12 +431,14 @@ pub struct Runnable {
 #[serde(untagged)]
 pub enum RunnableArgs {
     Cargo(CargoRunnableArgs),
+    Shell(ShellRunnableArgs),
 }
 
 #[derive(Serialize, Deserialize, Debug)]
 #[serde(rename_all = "lowercase")]
 pub enum RunnableKind {
     Cargo,
+    Shell,
 }
 
 #[derive(Deserialize, Serialize, Debug)]
@@ -456,6 +458,14 @@ pub struct CargoRunnableArgs {
     pub expect_test: Option<bool>,
 }
 
+#[derive(Deserialize, Serialize, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct ShellRunnableArgs {
+    pub program: String,
+    pub args: Vec<String>,
+    pub cwd: PathBuf,
+}
+
 pub enum RelatedTests {}
 
 impl Request for RelatedTests {
diff --git a/crates/rust-analyzer/src/lsp/to_proto.rs b/crates/rust-analyzer/src/lsp/to_proto.rs
index d1f5d23afedc..fc01c7987f24 100644
--- a/crates/rust-analyzer/src/lsp/to_proto.rs
+++ b/crates/rust-analyzer/src/lsp/to_proto.rs
@@ -24,6 +24,7 @@ use crate::{
     global_state::GlobalStateSnapshot,
     line_index::{LineEndings, LineIndex, PositionEncoding},
     lsp::{
+        ext::ShellRunnableArgs,
         semantic_tokens::{self, standard_fallback_type},
         utils::invalid_params_error,
         LspError,
@@ -1371,6 +1372,27 @@ pub(crate) fn runnable(
                 }),
             }))
         }
+        Some(TargetSpec::ProjectJson(spec)) => {
+            let label = runnable.label(Some(spec.label.clone()));
+            let location = location_link(snap, None, runnable.nav)?;
+
+            match spec.runnable_args(&runnable.kind) {
+                Some(json_shell_runnable_args) => {
+                    let runnable_args = ShellRunnableArgs {
+                        program: json_shell_runnable_args.program,
+                        args: json_shell_runnable_args.args,
+                        cwd: json_shell_runnable_args.cwd,
+                    };
+                    Ok(Some(lsp_ext::Runnable {
+                        label,
+                        location: Some(location),
+                        kind: lsp_ext::RunnableKind::Shell,
+                        args: lsp_ext::RunnableArgs::Shell(runnable_args),
+                    }))
+                }
+                None => Ok(None),
+            }
+        }
         None => {
             let (cargo_args, executable_args) =
                 CargoTargetSpec::runnable_args(snap, None, &runnable.kind, &runnable.cfg);
@@ -1418,6 +1440,7 @@ pub(crate) fn code_lens(
             if let Some(r) = r {
                 let has_root = match &r.args {
                     lsp_ext::RunnableArgs::Cargo(c) => c.workspace_root.is_some(),
+                    lsp_ext::RunnableArgs::Shell(_) => true,
                 };
 
                 let lens_config = snap.config.lens();
diff --git a/crates/rust-analyzer/src/target_spec.rs b/crates/rust-analyzer/src/target_spec.rs
index d2e518cf3a75..5565562f93f3 100644
--- a/crates/rust-analyzer/src/target_spec.rs
+++ b/crates/rust-analyzer/src/target_spec.rs
@@ -4,6 +4,8 @@ use std::mem;
 
 use cfg::{CfgAtom, CfgExpr};
 use ide::{Cancellable, CrateId, FileId, RunnableKind, TestId};
+use project_model::project_json::ShellRunnableArgs;
+use project_model::project_json::ShellRunnableKind;
 use project_model::{CargoFeatures, ManifestPath, TargetKind};
 use rustc_hash::FxHashSet;
 use vfs::AbsPathBuf;
@@ -17,6 +19,7 @@ use crate::global_state::GlobalStateSnapshot;
 #[derive(Clone)]
 pub(crate) enum TargetSpec {
     Cargo(CargoTargetSpec),
+    ProjectJson(ProjectJsonTargetSpec),
 }
 
 impl TargetSpec {
@@ -35,6 +38,7 @@ impl TargetSpec {
     pub(crate) fn target_kind(&self) -> TargetKind {
         match self {
             TargetSpec::Cargo(cargo) => cargo.target_kind,
+            TargetSpec::ProjectJson(project_json) => project_json.target_kind,
         }
     }
 }
@@ -55,6 +59,33 @@ pub(crate) struct CargoTargetSpec {
     pub(crate) features: FxHashSet<String>,
 }
 
+#[derive(Clone)]
+pub(crate) struct ProjectJsonTargetSpec {
+    pub(crate) label: String,
+    pub(crate) target_kind: TargetKind,
+    pub(crate) shell_runnables: Vec<ShellRunnableArgs>,
+}
+
+impl ProjectJsonTargetSpec {
+    pub(crate) fn runnable_args(&self, kind: &RunnableKind) -> Option<ShellRunnableArgs> {
+        match kind {
+            RunnableKind::Bin => {
+                for runnable in &self.shell_runnables {
+                    if matches!(runnable.kind, ShellRunnableKind::Run) {
+                        return Some(runnable.clone());
+                    }
+                }
+
+                None
+            }
+            RunnableKind::Test { .. } => None,
+            RunnableKind::TestMod { .. } => None,
+            RunnableKind::Bench { .. } => None,
+            RunnableKind::DocTest { .. } => None,
+        }
+    }
+}
+
 impl CargoTargetSpec {
     pub(crate) fn runnable_args(
         snap: &GlobalStateSnapshot,