diff --git a/src/components/setting/mods/hotkey-input.tsx b/src/components/setting/mods/hotkey-input.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..63b53eb103aaaeaca57d5b2a928e7d5d29d70280
--- /dev/null
+++ b/src/components/setting/mods/hotkey-input.tsx
@@ -0,0 +1,96 @@
+import { useState } from "react";
+import { alpha, Box, IconButton, styled } from "@mui/material";
+import { DeleteRounded } from "@mui/icons-material";
+import parseHotkey from "@/utils/parse-hotkey";
+
+const KeyWrapper = styled("div")(({ theme }) => ({
+  position: "relative",
+  width: 165,
+  minHeight: 36,
+
+  "> input": {
+    position: "absolute",
+    top: 0,
+    left: 0,
+    width: "100%",
+    height: "100%",
+    zIndex: 1,
+    opacity: 0,
+  },
+  "> input:focus + .list": {
+    borderColor: alpha(theme.palette.primary.main, 0.75),
+  },
+  ".list": {
+    display: "flex",
+    alignItems: "center",
+    flexWrap: "wrap",
+    width: "100%",
+    height: "100%",
+    minHeight: 36,
+    boxSizing: "border-box",
+    padding: "3px 4px",
+    border: "1px solid",
+    borderRadius: 4,
+    borderColor: alpha(theme.palette.text.secondary, 0.15),
+    "&:last-child": {
+      marginRight: 0,
+    },
+  },
+  ".item": {
+    color: theme.palette.text.primary,
+    border: "1px solid",
+    borderColor: alpha(theme.palette.text.secondary, 0.2),
+    borderRadius: "2px",
+    padding: "1px 1px",
+    margin: "2px 0",
+    marginRight: 8,
+  },
+}));
+
+interface Props {
+  value: string[];
+  onChange: (value: string[]) => void;
+}
+
+const HotkeyInput = (props: Props) => {
+  const { value, onChange } = props;
+
+  return (
+    <Box sx={{ display: "flex", alignItems: "center" }}>
+      <KeyWrapper>
+        <input
+          onKeyDown={(e) => {
+            const evt = e.nativeEvent;
+            e.preventDefault();
+            e.stopPropagation();
+
+            const key = parseHotkey(evt.key);
+            if (key === "UNIDENTIFIED") return;
+
+            const newList = [...new Set([...value, key])];
+            onChange(newList);
+          }}
+        />
+
+        <div className="list">
+          {value.map((key) => (
+            <div key={key} className="item">
+              {key}
+            </div>
+          ))}
+        </div>
+      </KeyWrapper>
+
+      <IconButton
+        size="small"
+        title="Delete"
+        color="inherit"
+        onClick={() => onChange([])}
+      >
+        <DeleteRounded fontSize="inherit" />
+      </IconButton>
+    </Box>
+  );
+};
+
+export default HotkeyInput;
diff --git a/src/components/setting/mods/hotkey-viewer.tsx b/src/components/setting/mods/hotkey-viewer.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..8a66f4f2b092233ea75b35fd9789ccca33d73154
--- /dev/null
+++ b/src/components/setting/mods/hotkey-viewer.tsx
@@ -0,0 +1,132 @@
+import useSWR from "swr";
+import { useEffect, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { useLockFn } from "ahooks";
+import {
+  Button,
+  Dialog,
+  DialogActions,
+  DialogContent,
+  DialogTitle,
+  styled,
+  Typography,
+} from "@mui/material";
+import { getVergeConfig, patchVergeConfig } from "@/services/cmds";
+import { ModalHandler } from "@/hooks/use-modal-handler";
+import Notice from "@/components/base/base-notice";
+import HotkeyInput from "./hotkey-input";
+
+const ItemWrapper = styled("div")`
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 8px;
+`;
+
+const HOTKEY_FUNC = [
+  "clash_mode_rule",
+  "clash_mode_direct",
+  "clash_mode_global",
+  "clash_moda_script",
+  "toggle_system_proxy",
+  "enable_system_proxy",
+  "disable_system_proxy",
+  "toggle_tun_mode",
+  "enable_tun_mode",
+  "disable_tun_mode",
+];
+
+interface Props {
+  handler: ModalHandler;
+}
+
+const HotkeyViewer = ({ handler }: Props) => {
+  const { t } = useTranslation();
+  const [open, setOpen] = useState(false);
+
+  if (handler) {
+    handler.current = {
+      open: () => setOpen(true),
+      close: () => setOpen(false),
+    };
+  }
+
+  const { data: vergeConfig, mutate: mutateVerge } = useSWR(
+    "getVergeConfig",
+    getVergeConfig
+  );
+
+  const [hotkeyMap, setHotkeyMap] = useState<Record<string, string[]>>({});
+
+  useEffect(() => {
+    if (!open) return;
+    const map = {} as typeof hotkeyMap;
+
+    vergeConfig?.hotkeys?.forEach((text) => {
+      const [func, key] = text.split(",").map((e) => e.trim());
+
+      if (!func || !key) return;
+
+      map[func] = key
+        .split("+")
+        .map((e) => e.trim())
+        .map((k) => (k === "PLUS" ? "+" : k));
+    });
+
+    setHotkeyMap(map);
+  }, [vergeConfig?.hotkeys, open]);
+
+  const onSave = useLockFn(async () => {
+    const hotkeys = Object.entries(hotkeyMap)
+      .map(([func, keys]) => {
+        if (!func || !keys?.length) return "";
+
+        const key = keys
+          .map((k) => k.trim())
+          .filter(Boolean)
+          .map((k) => (k === "+" ? "PLUS" : k))
+          .join("+");
+
+        if (!key) return "";
+        return `${func},${key}`;
+      })
+      .filter(Boolean);
+
+    try {
+      patchVergeConfig({ hotkeys });
+      setOpen(false);
+      mutateVerge();
+    } catch (err: any) {
+      Notice.error(err.message || err.toString());
+    }
+  });
+
+  return (
+    <Dialog open={open} onClose={() => setOpen(false)}>
+      <DialogTitle>{t("Hotkey Viewer")}</DialogTitle>
+
+      <DialogContent sx={{ width: 450, maxHeight: 330 }}>
+        {HOTKEY_FUNC.map((func) => (
+          <ItemWrapper key={func}>
+            <Typography>{t(func)}</Typography>
+            <HotkeyInput
+              value={hotkeyMap[func] ?? []}
+              onChange={(v) => setHotkeyMap((m) => ({ ...m, [func]: v }))}
+            />
+          </ItemWrapper>
+        ))}
+      </DialogContent>
+
+      <DialogActions>
+        <Button variant="outlined" onClick={() => setOpen(false)}>
+          {t("Cancel")}
+        </Button>
+        <Button onClick={onSave} variant="contained">
+          {t("Save")}
+        </Button>
+      </DialogActions>
+    </Dialog>
+  );
+};
+
+export default HotkeyViewer;
diff --git a/src/components/setting/setting-verge.tsx b/src/components/setting/setting-verge.tsx
index ae3b69d6073783e2cb3b0f372f3aacb1e5eb4ec5..8eaff19c63ae83c4431d3ac306ab65c8e466e548 100644
--- a/src/components/setting/setting-verge.tsx
+++ b/src/components/setting/setting-verge.tsx
@@ -17,8 +17,10 @@ import {
 import { ArrowForward } from "@mui/icons-material";
 import { SettingList, SettingItem } from "./setting";
 import { version } from "@root/package.json";
+import useModalHandler from "@/hooks/use-modal-handler";
 import ThemeModeSwitch from "./mods/theme-mode-switch";
 import ConfigViewer from "./mods/config-viewer";
+import HotkeyViewer from "./mods/hotkey-viewer";
 import GuardState from "./mods/guard-state";
 import SettingTheme from "./setting-theme";
 
@@ -43,8 +45,12 @@ const SettingVerge = ({ onError }: Props) => {
     mutateVerge({ ...vergeConfig, ...patch }, false);
   };
 
+  const hotkeyHandler = useModalHandler();
+
   return (
     <SettingList title={t("Verge Setting")}>
+      <HotkeyViewer handler={hotkeyHandler} />
+
       <SettingItem label={t("Language")}>
         <GuardState
           value={language ?? "en"}
@@ -108,6 +114,17 @@ const SettingVerge = ({ onError }: Props) => {
         </IconButton>
       </SettingItem>
 
+      <SettingItem label={t("Hotkey Setting")}>
+        <IconButton
+          color="inherit"
+          size="small"
+          sx={{ my: "2px" }}
+          onClick={() => hotkeyHandler.current.open()}
+        >
+          <ArrowForward />
+        </IconButton>
+      </SettingItem>
+
       <SettingItem label={t("Runtime Config")}>
         <IconButton
           color="inherit"
diff --git a/src/services/types.d.ts b/src/services/types.d.ts
index fa2604813d9fb444e80fe9f491a076f0360114cb..6d18acdf001afac61a9dc75e843bc2deb052e77e 100644
--- a/src/services/types.d.ts
+++ b/src/services/types.d.ts
@@ -147,6 +147,7 @@ declare namespace CmdType {
     proxy_guard_duration?: number;
     system_proxy_bypass?: string;
     web_ui_list?: string[];
+    hotkeys?: string[];
     theme_setting?: {
       primary_color?: string;
       secondary_color?: string;
diff --git a/src/utils/parse-hotkey.ts b/src/utils/parse-hotkey.ts
new file mode 100644
index 0000000000000000000000000000000000000000..01addd0583a86b8490fdae995aa3dffd94b8a243
--- /dev/null
+++ b/src/utils/parse-hotkey.ts
@@ -0,0 +1,29 @@
+const parseHotkey = (key: string) => {
+  let temp = key.toUpperCase();
+
+  if (temp.startsWith("ARROW")) {
+    temp = temp.slice(5);
+  } else if (temp.startsWith("DIGIT")) {
+    temp = temp.slice(5);
+  } else if (temp.startsWith("KEY")) {
+    temp = temp.slice(3);
+  } else if (temp.endsWith("LEFT")) {
+    temp = temp.slice(0, -4);
+  } else if (temp.endsWith("RIGHT")) {
+    temp = temp.slice(0, -5);
+  }
+
+  switch (temp) {
+    case "CONTROL":
+      return "CTRL";
+    case "META":
+      return "CMD";
+    case " ":
+      return "SPACE";
+
+    default:
+      return temp;
+  }
+};
+
+export default parseHotkey;