diff --git a/package.json b/package.json
index 04542f61535679dab862d9cb7eb0da4fc928b68b..3e5d2cd3f648c273e43c92abe963798bc679ca9b 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,8 @@
     "build": "cargo tauri build",
     "web:dev": "vite",
     "web:build": "tsc && vite build",
-    "web:serve": "vite preview"
+    "web:serve": "vite preview",
+    "predev": "node scripts/pre-dev.mjs"
   },
   "dependencies": {
     "@emotion/react": "^11.7.0",
@@ -29,6 +30,9 @@
     "@types/react": "^17.0.0",
     "@types/react-dom": "^17.0.0",
     "@vitejs/plugin-react": "^1.1.1",
+    "adm-zip": "^0.5.9",
+    "fs-extra": "^10.0.0",
+    "node-fetch": "^3.1.0",
     "sass": "^1.44.0",
     "typescript": "^4.5.2",
     "vite": "^2.7.1"
diff --git a/scripts/pre-dev.mjs b/scripts/pre-dev.mjs
new file mode 100644
index 0000000000000000000000000000000000000000..982e4ce90814e2ad2a34c02b01f15d958fa16427
--- /dev/null
+++ b/scripts/pre-dev.mjs
@@ -0,0 +1,104 @@
+import fs from "fs-extra";
+import path from "path";
+import AdmZip from "adm-zip";
+import fetch from "node-fetch";
+import { execSync } from "child_process";
+
+const cwd = process.cwd();
+
+const CLASH_URL_PREFIX =
+  "https://github.com/Dreamacro/clash/releases/download/premium/";
+const CLASH_LATEST_DATE = "2021.12.07";
+
+/**
+ * get the correct clash release infomation
+ */
+function resolveClash() {
+  const { platform, arch } = process;
+
+  let name = "";
+
+  // todo
+  if (platform === "win32" && arch === "x64") {
+    name = `clash-windows-386`;
+  }
+
+  if (!name) {
+    throw new Error("todo");
+  }
+
+  const isWin = platform === "win32";
+  const zip = isWin ? "zip" : "gz";
+  const url = `${CLASH_URL_PREFIX}${name}-${CLASH_LATEST_DATE}.${zip}`;
+  const exefile = `${name}${isWin ? ".exe" : ""}`;
+  const zipfile = `${name}.${zip}`;
+
+  return { url, zip, exefile, zipfile };
+}
+
+/**
+ * get the sidecar bin
+ */
+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);
+
+  if (!(await fs.pathExists(sidecarDir))) await fs.mkdir(sidecarDir);
+  if (await fs.pathExists(sidecarPath)) return;
+
+  // download sidecar
+  const binInfo = resolveClash();
+  const tempDir = path.join(cwd, "pre-dev-temp");
+  const tempZip = path.join(tempDir, binInfo.zipfile);
+  const tempExe = path.join(tempDir, binInfo.exefile);
+
+  if (!(await fs.pathExists(tempDir))) await fs.mkdir(tempDir);
+  if (!(await fs.pathExists(tempZip))) await downloadFile(binInfo.url, tempZip);
+
+  // Todo: support gz
+  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);
+
+  // delete temp dir
+  await fs.remove(tempDir);
+}
+
+/**
+ * get the Country.mmdb (not required)
+ */
+async function resolveMmdb() {
+  const url =
+    "https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb";
+
+  const resPath = path.join(cwd, "src-tauri", "resources", "Country.mmdb");
+  if (await fs.pathExists(resPath)) return;
+  await downloadFile(url, resPath);
+}
+
+/**
+ * download file and save to `path`
+ */
+async function downloadFile(url, path) {
+  console.log(`[INFO]: downloading from "${url}"`);
+
+  const response = await fetch(url, {
+    method: "GET",
+    headers: { "Content-Type": "application/octet-stream" },
+  });
+  const buffer = await response.arrayBuffer();
+  await fs.writeFile(path, new Uint8Array(buffer));
+}
+
+/// main
+resolveSidecar();
+resolveMmdb();