diff --git a/src-tauri/src/cmds.rs b/src-tauri/src/cmds.rs
index 8f54fb305c46106db4f3daf061a44cfcc601bee6..8cf1b54e3ac0cfe59441ceb9cac3bc0401f8edca 100644
--- a/src-tauri/src/cmds.rs
+++ b/src-tauri/src/cmds.rs
@@ -43,6 +43,42 @@ pub async fn import_profile(
   }
 }
 
+/// new a profile
+/// append a temp profile item file to the `profiles` dir
+/// view the temp profile file by using vscode or other editor
+#[tauri::command]
+pub async fn new_profile(
+  name: String,
+  desc: String,
+  profiles_state: State<'_, ProfilesState>,
+) -> Result<(), String> {
+  let mut profiles = profiles_state.0.lock().unwrap();
+
+  let (_, path) = profiles.append_item(name, desc)?;
+
+  if !path.exists() {
+    return Err("the file not found".into());
+  }
+
+  // use vscode first
+  if let Ok(code) = which::which("code") {
+    return match Command::new(code).arg(path).status() {
+      Ok(_) => Ok(()),
+      Err(_) => Err("failed to open file by VScode".into()),
+    };
+  }
+
+  // use `open` command
+  if let Ok(open) = which::which("open") {
+    return match Command::new(open).arg(path).status() {
+      Ok(_) => Ok(()),
+      Err(_) => Err("failed to open file by `open`".into()),
+    };
+  }
+
+  return Err("failed to open the file, please edit the file manually".into());
+}
+
 /// Update the profile
 #[tauri::command]
 pub async fn update_profile(
diff --git a/src-tauri/src/core/profiles.rs b/src-tauri/src/core/profiles.rs
index 96093c7f47e6af38ffe4b9a85c63ba90767f501d..3f30ffa0ee49d80bd5a800858e1f9229ae13f993 100644
--- a/src-tauri/src/core/profiles.rs
+++ b/src-tauri/src/core/profiles.rs
@@ -7,6 +7,7 @@ use std::collections::HashMap;
 use std::env::temp_dir;
 use std::fs::File;
 use std::io::Write;
+use std::path::PathBuf;
 use std::time::{SystemTime, UNIX_EPOCH};
 
 /// Define the `profiles.yaml` schema
@@ -23,6 +24,8 @@ pub struct Profiles {
 pub struct ProfileItem {
   /// profile name
   pub name: Option<String>,
+  /// profile description
+  pub desc: Option<String>,
   /// profile file
   pub file: Option<String>,
   /// current mode
@@ -109,6 +112,7 @@ impl Profiles {
 
     items.push(ProfileItem {
       name: Some(result.name),
+      desc: Some("imported url".into()),
       file: Some(result.file),
       mode: Some(format!("rule")),
       url: Some(url),
@@ -138,6 +142,49 @@ impl Profiles {
     self.save_file()
   }
 
+  /// append new item
+  /// return the new item's index
+  pub fn append_item(&mut self, name: String, desc: String) -> Result<(usize, PathBuf), String> {
+    let mut items = self.items.take().unwrap_or(vec![]);
+
+    // create a new profile file
+    let now = SystemTime::now()
+      .duration_since(UNIX_EPOCH)
+      .unwrap()
+      .as_secs();
+    let file = format!("{}.yaml", now);
+    let path = dirs::app_home_dir().join("profiles").join(&file);
+
+    let file_data = b"# Profile Template for clash verge\n
+# proxies defination (optional, the same as clash)
+proxies:\n
+# proxy-groups (optional, the same as clash)
+proxy-groups:\n
+# rules (optional, the same as clash)
+rules:\n\n
+";
+
+    match File::create(&path).unwrap().write(file_data) {
+      Ok(_) => {
+        items.push(ProfileItem {
+          name: Some(name),
+          desc: Some(desc),
+          file: Some(file),
+          mode: None,
+          url: None,
+          selected: Some(vec![]),
+          extra: None,
+          updated: Some(now as usize),
+        });
+
+        let index = items.len();
+        self.items = Some(items);
+        Ok((index, path))
+      }
+      Err(_) => Err("failed to create file".into()),
+    }
+  }
+
   /// update the target profile
   /// and save to config file
   /// only support the url item
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs
index ba743a3830d90a1998a9d15e639e5b2f795079ce..3184c870f31f75f13ca3024e681ca3dabdac3f1a 100644
--- a/src-tauri/src/main.rs
+++ b/src-tauri/src/main.rs
@@ -81,6 +81,7 @@ fn main() -> std::io::Result<()> {
       cmds::get_verge_config,
       cmds::patch_verge_config,
       // profile
+      cmds::new_profile,
       cmds::view_profile,
       cmds::patch_profile,
       cmds::import_profile,
diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx
index 75a26393557f20ce19994832c754e00630ac4592..299658a21993c64a2ffb332136dcedba87a6643f 100644
--- a/src/pages/profiles.tsx
+++ b/src/pages/profiles.tsx
@@ -6,6 +6,7 @@ import {
   selectProfile,
   patchProfile,
   importProfile,
+  newProfile,
 } from "../services/cmds";
 import { getProxies, updateProxy } from "../services/api";
 import noop from "../utils/noop";
@@ -94,6 +95,21 @@ const ProfilePage = () => {
     }
   };
 
+  const lockNewRef = useRef(false);
+  const onNew = async () => {
+    if (lockNewRef.current) return;
+    lockNewRef.current = true;
+
+    try {
+      await newProfile("New Profile", "no desc");
+      mutate("getProfiles");
+    } catch (err: any) {
+      err && Notice.error(err.toString());
+    } finally {
+      lockNewRef.current = false;
+    }
+  };
+
   return (
     <BasePage title="Profiles">
       <Box sx={{ display: "flex", mb: 3 }}>
@@ -105,15 +121,19 @@ const ProfilePage = () => {
           fullWidth
           value={url}
           onChange={(e) => setUrl(e.target.value)}
-          sx={{ mr: 2 }}
+          sx={{ mr: 1 }}
         />
         <Button
           disabled={!url || disabled}
           variant="contained"
           onClick={onImport}
+          sx={{ mr: 1 }}
         >
           Import
         </Button>
+        <Button variant="contained" onClick={onNew}>
+          New
+        </Button>
       </Box>
 
       <Grid container spacing={3}>
diff --git a/src/services/cmds.ts b/src/services/cmds.ts
index 6f5703f903e378e77d14db9160411328993dc104..5f9cf0858dfa5134d2d2e71d1b8b113877f5ea48 100644
--- a/src/services/cmds.ts
+++ b/src/services/cmds.ts
@@ -9,6 +9,10 @@ export async function syncProfiles() {
   return invoke<void>("sync_profiles");
 }
 
+export async function newProfile(name: string, desc: string) {
+  return invoke<void>("new_profile", { name, desc });
+}
+
 export async function viewProfile(index: number) {
   return invoke<void>("view_profile", { index });
 }