diff --git a/book/src/languages.md b/book/src/languages.md
index 7e49a603..e3900dca 100644
--- a/book/src/languages.md
+++ b/book/src/languages.md
@@ -122,13 +122,14 @@ languages = { typescript = [ { formatCommand ="prettier --stdin-filepath ${INPUT
 
 These are the available options for a language server.
 
-| Key                   | Description                                                                              |
-| ----                  | -----------                                                                              |
-| `command`             | The name or path of the language server binary to execute. Binaries must be in `$PATH`   |
-| `args`                | A list of arguments to pass to the language server binary                                |
-| `config`              | LSP initialization options                               |
-| `timeout`             | The maximum time a request to the language server may take, in seconds. Defaults to `20` |
-| `environment`         | Any environment variables that will be used when starting the language server `{ "KEY1" = "Value1", "KEY2" = "Value2" }` |
+| Key                        | Description                                                                                                                       |
+| ----                       | -----------                                                                                                                       |
+| `command`                  | The name or path of the language server binary to execute. Binaries must be in `$PATH`                                            |
+| `args`                     | A list of arguments to pass to the language server binary                                                                         |
+| `config`                   | LSP initialization options                                                                                                        |
+| `timeout`                  | The maximum time a request to the language server may take, in seconds. Defaults to `20`                                          |
+| `environment`              | Any environment variables that will be used when starting the language server `{ "KEY1" = "Value1", "KEY2" = "Value2" }`          |
+| `required-root-patterns`   | A list of `glob` patterns to look for in the working directory. The language server is started if at least one of them is found.  |
 
 A `format` sub-table within `config` can be used to pass extra formatting options to
 [Document Formatting Requests](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_formatting).
diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml
index bdc879ca..e0eb6401 100644
--- a/helix-core/Cargo.toml
+++ b/helix-core/Cargo.toml
@@ -53,6 +53,7 @@ globset = "0.4.14"
 
 nucleo.workspace = true
 parking_lot = "0.12"
+globset = "0.4.14"
 
 [dev-dependencies]
 quickcheck = { version = "1", default-features = false }
diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs
index 99b5a3d1..5d45deaf 100644
--- a/helix-core/src/syntax.rs
+++ b/helix-core/src/syntax.rs
@@ -10,6 +10,7 @@ use crate::{
 use ahash::RandomState;
 use arc_swap::{ArcSwap, Guard};
 use bitflags::bitflags;
+use globset::GlobSet;
 use hashbrown::raw::RawTable;
 use slotmap::{DefaultKey as LayerId, HopSlotMap};
 
@@ -365,6 +366,22 @@ where
     serializer.end()
 }
 
+fn deserialize_required_root_patterns<'de, D>(deserializer: D) -> Result<Option<GlobSet>, D::Error>
+where
+    D: serde::Deserializer<'de>,
+{
+    let patterns = Vec::<String>::deserialize(deserializer)?;
+    if patterns.is_empty() {
+        return Ok(None);
+    }
+    let mut builder = globset::GlobSetBuilder::new();
+    for pattern in patterns {
+        let glob = globset::Glob::new(&pattern).map_err(serde::de::Error::custom)?;
+        builder.add(glob);
+    }
+    builder.build().map(Some).map_err(serde::de::Error::custom)
+}
+
 #[derive(Debug, Serialize, Deserialize)]
 #[serde(rename_all = "kebab-case")]
 pub struct LanguageServerConfiguration {
@@ -378,6 +395,12 @@ pub struct LanguageServerConfiguration {
     pub config: Option<serde_json::Value>,
     #[serde(default = "default_timeout")]
     pub timeout: u64,
+    #[serde(
+        default,
+        skip_serializing,
+        deserialize_with = "deserialize_required_root_patterns"
+    )]
+    pub required_root_patterns: Option<GlobSet>,
 }
 
 #[derive(Debug, Clone, Serialize, Deserialize)]
diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs
index 94bad6fa..0d3a2a56 100644
--- a/helix-lsp/src/client.rs
+++ b/helix-lsp/src/client.rs
@@ -177,12 +177,11 @@ impl Client {
         args: &[String],
         config: Option<Value>,
         server_environment: HashMap<String, String>,
-        root_markers: &[String],
-        manual_roots: &[PathBuf],
+        root_path: PathBuf,
+        root_uri: Option<lsp::Url>,
         id: usize,
         name: String,
         req_timeout: u64,
-        doc_path: Option<&std::path::PathBuf>,
     ) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc<Notify>)> {
         // Resolve path to the binary
         let cmd = helix_stdx::env::which(cmd)?;
@@ -206,22 +205,6 @@ impl Client {
 
         let (server_rx, server_tx, initialize_notify) =
             Transport::start(reader, writer, stderr, id, name.clone());
-        let (workspace, workspace_is_cwd) = find_workspace();
-        let workspace = path::normalize(workspace);
-        let root = find_lsp_workspace(
-            doc_path
-                .and_then(|x| x.parent().and_then(|x| x.to_str()))
-                .unwrap_or("."),
-            root_markers,
-            manual_roots,
-            &workspace,
-            workspace_is_cwd,
-        );
-
-        // `root_uri` and `workspace_folder` can be empty in case there is no workspace
-        // `root_url` can not, use `workspace` as a fallback
-        let root_path = root.clone().unwrap_or_else(|| workspace.clone());
-        let root_uri = root.and_then(|root| lsp::Url::from_file_path(root).ok());
 
         let workspace_folders = root_uri
             .clone()
diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs
index 4ce445ae..05764418 100644
--- a/helix-lsp/src/lib.rs
+++ b/helix-lsp/src/lib.rs
@@ -680,7 +680,7 @@ impl Registry {
         doc_path: Option<&std::path::PathBuf>,
         root_dirs: &[PathBuf],
         enable_snippets: bool,
-    ) -> Result<Arc<Client>> {
+    ) -> Result<Option<Arc<Client>>> {
         let config = self
             .syn_loader
             .language_server_configs()
@@ -688,7 +688,7 @@ impl Registry {
             .ok_or_else(|| anyhow::anyhow!("Language server '{name}' not defined"))?;
         let id = self.counter;
         self.counter += 1;
-        let NewClient(client, incoming) = start_client(
+        if let Some(NewClient(client, incoming)) = start_client(
             id,
             name,
             ls_config,
@@ -696,9 +696,12 @@ impl Registry {
             doc_path,
             root_dirs,
             enable_snippets,
-        )?;
-        self.incoming.push(UnboundedReceiverStream::new(incoming));
-        Ok(client)
+        )? {
+            self.incoming.push(UnboundedReceiverStream::new(incoming));
+            Ok(Some(client))
+        } else {
+            Ok(None)
+        }
     }
 
     /// If this method is called, all documents that have a reference to language servers used by the language config have to refresh their language servers,
@@ -723,8 +726,8 @@ impl Registry {
                         root_dirs,
                         enable_snippets,
                     ) {
-                        Ok(client) => client,
-                        error => return Some(error),
+                        Ok(client) => client?,
+                        Err(error) => return Some(Err(error)),
                     };
                     let old_clients = self
                         .inner
@@ -764,13 +767,13 @@ impl Registry {
         root_dirs: &'a [PathBuf],
         enable_snippets: bool,
     ) -> impl Iterator<Item = (LanguageServerName, Result<Arc<Client>>)> + 'a {
-        language_config.language_servers.iter().map(
+        language_config.language_servers.iter().filter_map(
             move |LanguageServerFeatures { name, .. }| {
                 if let Some(clients) = self.inner.get(name) {
                     if let Some((_, client)) = clients.iter().enumerate().find(|(i, client)| {
                         client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0)
                     }) {
-                        return (name.to_owned(), Ok(client.clone()));
+                        return Some((name.to_owned(), Ok(client.clone())));
                     }
                 }
                 match self.start_client(
@@ -781,13 +784,14 @@ impl Registry {
                     enable_snippets,
                 ) {
                     Ok(client) => {
+                        let client = client?;
                         self.inner
                             .entry(name.to_owned())
                             .or_default()
                             .push(client.clone());
-                        (name.clone(), Ok(client))
+                        Some((name.clone(), Ok(client)))
                     }
-                    Err(err) => (name.to_owned(), Err(err)),
+                    Err(err) => Some((name.to_owned(), Err(err))),
                 }
             },
         )
@@ -888,18 +892,45 @@ fn start_client(
     doc_path: Option<&std::path::PathBuf>,
     root_dirs: &[PathBuf],
     enable_snippets: bool,
-) -> Result<NewClient> {
+) -> Result<Option<NewClient>> {
+    let (workspace, workspace_is_cwd) = helix_loader::find_workspace();
+    let workspace = path::normalize(workspace);
+    let root = find_lsp_workspace(
+        doc_path
+            .and_then(|x| x.parent().and_then(|x| x.to_str()))
+            .unwrap_or("."),
+        &config.roots,
+        config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs),
+        &workspace,
+        workspace_is_cwd,
+    );
+
+    // `root_uri` and `workspace_folder` can be empty in case there is no workspace
+    // `root_url` can not, use `workspace` as a fallback
+    let root_path = root.clone().unwrap_or_else(|| workspace.clone());
+    let root_uri = root.and_then(|root| lsp::Url::from_file_path(root).ok());
+
+    if let Some(globset) = &ls_config.required_root_patterns {
+        if !root_path
+            .read_dir()?
+            .flatten()
+            .map(|entry| entry.file_name())
+            .any(|entry| globset.is_match(entry))
+        {
+            return Ok(None);
+        }
+    }
+
     let (client, incoming, initialize_notify) = Client::start(
         &ls_config.command,
         &ls_config.args,
         ls_config.config.clone(),
         ls_config.environment.clone(),
-        &config.roots,
-        config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs),
+        root_path,
+        root_uri,
         id,
         name,
         ls_config.timeout,
-        doc_path,
     )?;
 
     let client = Arc::new(client);
@@ -938,7 +969,7 @@ fn start_client(
         initialize_notify.notify_one();
     });
 
-    Ok(NewClient(client, incoming))
+    Ok(Some(NewClient(client, incoming)))
 }
 
 /// Find an LSP workspace of a file using the following mechanism: