diff --git a/scripts/check.mjs b/scripts/check.mjs
index b662cddb489a8a61708c66289da28bb8b500b23c..1b44b9568a92aa3314e643b545d0cf0b15ba09fb 100644
--- a/scripts/check.mjs
+++ b/scripts/check.mjs
@@ -10,6 +10,7 @@ const cwd = process.cwd();
 const TEMP_DIR = path.join(cwd, "node_modules/.verge");
 
 const FORCE = process.argv.includes("--force");
+const META = process.argv.includes("--meta"); // use Clash.Meta
 
 /**
  * get the correct clash release infomation
@@ -44,55 +45,139 @@ function resolveClash() {
   return { url, zip, exefile, zipfile };
 }
 
+/**
+ * get the correct Clash.Meta release infomation
+ */
+async function resolveClashMeta() {
+  const { platform, arch } = process;
+
+  const urlPrefix = `https://github.com/MetaCubeX/Clash.Meta/releases/download/`;
+  const latestVersion = "v1.11.0";
+
+  const map = {
+    "win32-x64": "Clash.Meta-windows-amd64v3",
+    "darwin-x64": "Clash.Meta-darwin-amd64v3",
+    "darwin-arm64": "Clash.Meta-darwin-arm64",
+    "linux-x64": "Clash.Meta-linux-amd64v3",
+  };
+
+  const name = map[`${platform}-${arch}`];
+
+  if (!name) {
+    throw new Error(`unsupport platform "${platform}-${arch}"`);
+  }
+
+  const isWin = platform === "win32";
+  const ext = isWin ? "zip" : "gz";
+  const url = `${urlPrefix}${latestVersion}/${name}-${latestVersion}.${ext}`;
+  const exefile = `${name}${isWin ? ".exe" : ""}`;
+  const zipfile = `${name}-${latestVersion}.${ext}`;
+
+  return { url, zip: ext, exefile, zipfile };
+}
+
 /**
  * get the sidecar bin
+ * clash and Clash Meta
  */
 async function resolveSidecar() {
   const sidecarDir = path.join(cwd, "src-tauri", "sidecar");
 
   const host = execSync("rustc -vV | grep host").toString().slice(6).trim();
   const ext = process.platform === "win32" ? ".exe" : "";
-  const sidecarFile = `clash-${host}${ext}`;
-  const sidecarPath = path.join(sidecarDir, sidecarFile);
-
-  await fs.mkdirp(sidecarDir);
-  if (!FORCE && (await fs.pathExists(sidecarPath))) return;
-
-  // download sidecar
-  const binInfo = resolveClash();
-  const tempDir = path.join(TEMP_DIR, "clash");
-  const tempZip = path.join(tempDir, binInfo.zipfile);
-  const tempExe = path.join(tempDir, binInfo.exefile);
 
-  await fs.mkdirp(tempDir);
-  if (!(await fs.pathExists(tempZip))) await downloadFile(binInfo.url, tempZip);
-
-  if (binInfo.zip === "zip") {
-    const zip = new AdmZip(tempZip);
-    zip.getEntries().forEach((entry) => {
-      console.log("[INFO]: entry name", entry.entryName);
-    });
-    zip.extractAllTo(tempDir, true);
-    // save as sidecar
-    await fs.rename(tempExe, sidecarPath);
-    console.log(`[INFO]: unzip finished`);
-  } else {
-    // gz
-    const readStream = fs.createReadStream(tempZip);
-    const writeStream = fs.createWriteStream(sidecarPath);
-    readStream
-      .pipe(zlib.createGunzip())
-      .pipe(writeStream)
-      .on("finish", () => {
-        console.log(`[INFO]: gunzip finished`);
-        execSync(`chmod 755 ${sidecarPath}`);
-        console.log(`[INFO]: chmod binary finished`);
-      })
-      .on("error", (error) => console.error(error));
+  await clash();
+  if (META) await clashMeta();
+
+  async function clash() {
+    const sidecarFile = `clash-${host}${ext}`;
+    const sidecarPath = path.join(sidecarDir, sidecarFile);
+
+    await fs.mkdirp(sidecarDir);
+    if (!FORCE && (await fs.pathExists(sidecarPath))) return;
+
+    // download sidecar
+    const binInfo = resolveClash();
+    const tempDir = path.join(TEMP_DIR, "clash");
+    const tempZip = path.join(tempDir, binInfo.zipfile);
+    const tempExe = path.join(tempDir, binInfo.exefile);
+
+    await fs.mkdirp(tempDir);
+    if (!(await fs.pathExists(tempZip)))
+      await downloadFile(binInfo.url, tempZip);
+
+    if (binInfo.zip === "zip") {
+      const zip = new AdmZip(tempZip);
+      zip.getEntries().forEach((entry) => {
+        console.log("[INFO]: entry name", entry.entryName);
+      });
+      zip.extractAllTo(tempDir, true);
+      // save as sidecar
+      await fs.rename(tempExe, sidecarPath);
+      console.log(`[INFO]: unzip finished`);
+    } else {
+      // gz
+      const readStream = fs.createReadStream(tempZip);
+      const writeStream = fs.createWriteStream(sidecarPath);
+      readStream
+        .pipe(zlib.createGunzip())
+        .pipe(writeStream)
+        .on("finish", () => {
+          console.log(`[INFO]: gunzip finished`);
+          execSync(`chmod 755 ${sidecarPath}`);
+          console.log(`[INFO]: chmod binary finished`);
+        })
+        .on("error", (error) => console.error(error));
+    }
+
+    // delete temp dir
+    await fs.remove(tempDir);
   }
 
-  // delete temp dir
-  await fs.remove(tempDir);
+  async function clashMeta() {
+    const sidecarFile = `clash-meta-${host}${ext}`;
+    const sidecarPath = path.join(sidecarDir, sidecarFile);
+
+    await fs.mkdirp(sidecarDir);
+    if (!FORCE && (await fs.pathExists(sidecarPath))) return;
+
+    // download sidecar
+    const binInfo = await resolveClashMeta();
+    const tempDir = path.join(TEMP_DIR, "clash-meta");
+    const tempZip = path.join(tempDir, binInfo.zipfile);
+    const tempExe = path.join(tempDir, binInfo.exefile);
+
+    await fs.mkdirp(tempDir);
+    if (!(await fs.pathExists(tempZip)))
+      await downloadFile(binInfo.url, tempZip);
+
+    if (binInfo.zip === "zip") {
+      const zip = new AdmZip(tempZip);
+      zip.getEntries().forEach((entry) => {
+        console.log("[INFO]: entry name", entry.entryName);
+      });
+      zip.extractAllTo(tempDir, true);
+      // save as sidecar
+      await fs.rename(tempExe, sidecarPath);
+      console.log(`[INFO]: unzip finished`);
+    } else {
+      // gz
+      const readStream = fs.createReadStream(tempZip);
+      const writeStream = fs.createWriteStream(sidecarPath);
+      readStream
+        .pipe(zlib.createGunzip())
+        .pipe(writeStream)
+        .on("finish", () => {
+          console.log(`[INFO]: gunzip finished`);
+          execSync(`chmod 755 ${sidecarPath}`);
+          console.log(`[INFO]: chmod binary finished`);
+        })
+        .on("error", (error) => console.error(error));
+    }
+
+    // delete temp dir
+    await fs.remove(tempDir);
+  }
 }
 
 /**
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index c734c046ba75f9bb5fc567bd282c5538adc76e73..14e02f12900b6079c829f1d15bff18d1542736a8 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -46,7 +46,9 @@ winreg = { version = "0.10", features = ["transactions"] }
 [features]
 default = ["custom-protocol", "tauri/ayatana-tray"]
 custom-protocol = ["tauri/custom-protocol"]
+meta-dev = ["clash-meta", "verge-dev"]
 verge-dev = []
+clash-meta = []
 debug-yml = []
 
 [profile.release]
diff --git a/src-tauri/src/core/service.rs b/src-tauri/src/core/service.rs
index 2a4f0558f1a4e37f1b303fdd84fcb1fa4f054dce..acde126350559bf4a4a9c9e69297179b4a2b6d82 100644
--- a/src-tauri/src/core/service.rs
+++ b/src-tauri/src/core/service.rs
@@ -9,6 +9,8 @@ use std::{collections::HashMap, time::Duration};
 use tauri::api::process::{Command, CommandChild, CommandEvent};
 use tokio::time::sleep;
 
+static mut CLASH_CORE: &str = "clash";
+
 #[derive(Debug)]
 pub struct Service {
   sidecar: Option<CommandChild>,
@@ -25,6 +27,12 @@ impl Service {
     }
   }
 
+  pub fn set_core(&mut self, clash_core: Option<String>) {
+    unsafe {
+      CLASH_CORE = Box::leak(clash_core.unwrap_or("clash".into()).into_boxed_str());
+    }
+  }
+
   #[allow(unused)]
   pub fn set_mode(&mut self, enable: bool) {
     self.service_mode = enable;
@@ -92,7 +100,8 @@ impl Service {
     let app_dir = dirs::app_home_dir();
     let app_dir = app_dir.as_os_str().to_str().unwrap();
 
-    let cmd = Command::new_sidecar("clash")?;
+    let clash_core = unsafe { CLASH_CORE };
+    let cmd = Command::new_sidecar(clash_core)?;
     let (mut rx, cmd_child) = cmd.args(["-d", app_dir]).spawn()?;
 
     self.sidecar = Some(cmd_child);
@@ -342,7 +351,9 @@ pub mod win_service {
         sleep(Duration::from_secs(1)).await;
       }
 
-      let bin_path = current_exe().unwrap().with_file_name("clash.exe");
+      let clash_core = unsafe { CLASH_CORE };
+      let clash_bin = format!("{clash_core}.exe");
+      let bin_path = current_exe().unwrap().with_file_name(clash_bin);
       let bin_path = bin_path.as_os_str().to_str().unwrap();
 
       let config_dir = dirs::app_home_dir();
diff --git a/src-tauri/src/core/verge.rs b/src-tauri/src/core/verge.rs
index a5bf19d6797a4e279035f53aeec60dbba1943d22..a03bde424376295fe671262678a19b850b3e6346 100644
--- a/src-tauri/src/core/verge.rs
+++ b/src-tauri/src/core/verge.rs
@@ -45,6 +45,10 @@ pub struct Verge {
 
   /// theme setting
   pub theme_setting: Option<VergeTheme>,
+
+  /// clash core path
+  #[serde(skip_serializing_if = "Option::is_none")]
+  pub clash_core: Option<String>,
 }
 
 #[derive(Default, Debug, Clone, Deserialize, Serialize)]
@@ -96,6 +100,9 @@ impl Verge {
     if patch.traffic_graph.is_some() {
       self.traffic_graph = patch.traffic_graph;
     }
+    if patch.clash_core.is_some() {
+      self.clash_core = patch.clash_core;
+    }
 
     // system setting
     if patch.enable_silent_start.is_some() {
diff --git a/src-tauri/tauri.meta.conf.json b/src-tauri/tauri.meta.conf.json
new file mode 100644
index 0000000000000000000000000000000000000000..9f4bb77979796ce1759622a37a0071fcf61c1490
--- /dev/null
+++ b/src-tauri/tauri.meta.conf.json
@@ -0,0 +1,7 @@
+{
+  "tauri": {
+    "bundle": {
+      "externalBin": ["sidecar/clash", "sidecar/clash-meta"]
+    }
+  }
+}