diff --git a/.github/workflows/test-cache.yml b/.github/workflows/test-cache.yml
index 0820ab7..7bff674 100644
--- a/.github/workflows/test-cache.yml
+++ b/.github/workflows/test-cache.yml
@@ -121,3 +121,49 @@ jobs:
           CACHE_HIT: ${{ steps.restore.outputs.cache-hit }}
       - run: uv sync
         working-directory: __tests__/fixtures/uv-project
+
+  prepare-tilde-expansion-tests:
+    runs-on: selfhosted-ubuntu-arm64
+    steps:
+      - name: Create cache directory
+        run: mkdir -p ~/uv-cache
+        shell: bash
+      - name: Create cache dependency glob file
+        run: touch ~/uv-cache.glob
+        shell: bash
+
+  test-tilde-expansion-cache-local-path:
+    needs: prepare-tilde-expansion-tests
+    runs-on: selfhosted-ubuntu-arm64
+    steps:
+      - uses: actions/checkout@v4
+      - name: Setup with cache
+        uses: ./
+        with:
+          enable-cache: true
+          cache-local-path: ~/uv-cache/cache-local-path
+
+  test-tilde-expansion-cache-dependency-glob:
+    needs: prepare-tilde-expansion-tests
+    runs-on: selfhosted-ubuntu-arm64
+    steps:
+      - uses: actions/checkout@v4
+      - name: Setup with cache
+        uses: ./
+        with:
+          enable-cache: true
+          cache-local-path: ~/uv-cache/cache-dependency-glob
+          cache-dependency-glob: "~/uv-cache.glob"
+
+  cleanup-tilde-expansion-tests:
+    needs:
+      - test-tilde-expansion-cache-local-path
+      - test-tilde-expansion-cache-dependency-glob
+    runs-on: selfhosted-ubuntu-arm64
+    steps:
+      - name: Remove cache directory
+        run: rm -rf ~/uv-cache
+        shell: bash
+      - name: Remove cache dependency glob file
+        run: rm -f ~/uv-cache.glob
+        shell: bash
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index d9e50f3..4306123 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -123,3 +123,24 @@ jobs:
         uses: ./
       - run: uv tool install ruff
       - run: ruff --version
+
+  test-tilde-expansion-tool-dirs:
+    runs-on: selfhosted-ubuntu-arm64
+    steps:
+      - uses: actions/checkout@v4
+      - name: Setup with cache
+        uses: ./
+        with:
+          tool-bin-dir: "~/tool-bin-dir"
+          tool-dir: "~/tool-dir"
+      - name: "Check if tool dirs are expanded"
+        run: |
+          if ! echo "$PATH" | grep -q "/home/ubuntu/tool-bin-dir"; then
+              echo "PATH does not contain /home/ubuntu/tool-bin-dir: $PATH"
+              exit 1
+          fi
+          if [ "$UV_TOOL_DIR" != "/home/ubuntu/tool-dir" ]; then
+              echo "UV_TOOL_DIR does not contain /home/ubuntu/tool-dir: $UV_TOOL_DIR"
+              exit 1
+          fi
+
diff --git a/README.md b/README.md
index 709c8ef..fbf26a4 100644
--- a/README.md
+++ b/README.md
@@ -21,6 +21,7 @@ Set up your GitHub Actions workflow with a specific version of [uv](https://docs
   - [GitHub authentication token](#github-authentication-token)
   - [UV_TOOL_DIR](#uv_tool_dir)
   - [UV_TOOL_BIN_DIR](#uv_tool_bin_dir)
+  - [Tilde Expansion](#tilde-expansion)
 - [How it works](#how-it-works)
 - [FAQ](#faq)
 
@@ -120,7 +121,7 @@ use it in subsequent steps. For example, to use the cache in the above case:
 
 If you want to control when the cache is invalidated, specify a glob pattern with the
 `cache-dependency-glob` input. The cache will be invalidated if any file matching the glob pattern
-changes. The glob matches files relative to the repository root.
+changes. If you use relative paths, the glob matches files relative to the repository root.
 
 > [!NOTE]
 >
@@ -144,6 +145,14 @@ changes. The glob matches files relative to the repository root.
       **/pyproject.toml
 ```
 
+```yaml
+- name: Define an absolute cache dependency glob
+  uses: astral-sh/setup-uv@v3
+  with:
+    enable-cache: true
+    cache-dependency-glob: "/tmp/my-folder/requirements*.txt"
+```
+
 ```yaml
 - name: Never invalidate the cache
   uses: astral-sh/setup-uv@v3
@@ -240,6 +249,25 @@ If you want to change this behaviour (especially on self-hosted runners) you can
     tool-bin-dir: "/path/to/tool-bin/dir"
 ```
 
+### Tilde Expansion
+
+This action supports expanding the `~` character to the user's home directory for the following inputs:
+
+- `cache-local-path`
+- `tool-dir`
+- `tool-bin-dir`
+- `cache-dependency-glob`
+
+```yaml
+- name: Expand the tilde character
+  uses: astral-sh/setup-uv@v3
+  with:
+    cache-local-path: "~/path/to/cache"
+    tool-dir: "~/path/to/tool/dir"
+    tool-bin-dir: "~/path/to/tool-bin/dir"
+    cache-dependency-glob: "~/my-cache-buster"
+```
+
 ## How it works
 
 This action downloads uv from the uv repo's official
diff --git a/dist/save-cache/index.js b/dist/save-cache/index.js
index 98132b7..13846ca 100644
--- a/dist/save-cache/index.js
+++ b/dist/save-cache/index.js
@@ -82304,10 +82304,10 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
 exports.STATE_CACHE_MATCHED_KEY = exports.STATE_CACHE_KEY = void 0;
 exports.restoreCache = restoreCache;
 const cache = __importStar(__nccwpck_require__(5116));
-const glob = __importStar(__nccwpck_require__(7206));
 const core = __importStar(__nccwpck_require__(7484));
 const inputs_1 = __nccwpck_require__(9612);
 const platforms_1 = __nccwpck_require__(8361);
+const hash_files_1 = __nccwpck_require__(9660);
 exports.STATE_CACHE_KEY = "cache-key";
 exports.STATE_CACHE_MATCHED_KEY = "cache-matched-key";
 const CACHE_VERSION = "1";
@@ -82334,7 +82334,7 @@ function computeKeys(version) {
         let cacheDependencyPathHash = "-";
         if (inputs_1.cacheDependencyGlob !== "") {
             core.info(`Searching files using cache dependency glob: ${inputs_1.cacheDependencyGlob.split("\n").join(",")}`);
-            cacheDependencyPathHash += yield glob.hashFiles(inputs_1.cacheDependencyGlob, undefined, undefined, true);
+            cacheDependencyPathHash += yield (0, hash_files_1.hashFiles)(inputs_1.cacheDependencyGlob, true);
             if (cacheDependencyPathHash === "-") {
                 throw new Error(`No file in ${process.cwd()} matched to [${inputs_1.cacheDependencyGlob.split("\n").join(",")}], make sure you have checked out the target repository`);
             }
@@ -82358,6 +82358,114 @@ function handleMatchResult(matchedKey, primaryKey) {
 }
 
 
+/***/ }),
+
+/***/ 9660:
+/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
+
+"use strict";
+
+var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
+    if (k2 === undefined) k2 = k;
+    var desc = Object.getOwnPropertyDescriptor(m, k);
+    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
+      desc = { enumerable: true, get: function() { return m[k]; } };
+    }
+    Object.defineProperty(o, k2, desc);
+}) : (function(o, m, k, k2) {
+    if (k2 === undefined) k2 = k;
+    o[k2] = m[k];
+}));
+var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
+    Object.defineProperty(o, "default", { enumerable: true, value: v });
+}) : function(o, v) {
+    o["default"] = v;
+});
+var __importStar = (this && this.__importStar) || function (mod) {
+    if (mod && mod.__esModule) return mod;
+    var result = {};
+    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
+    __setModuleDefault(result, mod);
+    return result;
+};
+var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
+    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
+    return new (P || (P = Promise))(function (resolve, reject) {
+        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
+        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
+        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
+        step((generator = generator.apply(thisArg, _arguments || [])).next());
+    });
+};
+var __asyncValues = (this && this.__asyncValues) || function (o) {
+    if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
+    var m = o[Symbol.asyncIterator], i;
+    return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
+    function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
+    function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
+};
+Object.defineProperty(exports, "__esModule", ({ value: true }));
+exports.hashFiles = hashFiles;
+const crypto = __importStar(__nccwpck_require__(7598));
+const core = __importStar(__nccwpck_require__(7484));
+const fs = __importStar(__nccwpck_require__(3024));
+const stream = __importStar(__nccwpck_require__(7075));
+const util = __importStar(__nccwpck_require__(7975));
+const glob_1 = __nccwpck_require__(7206);
+/**
+ * Hashes files matching the given glob pattern.
+ *
+ * Copied from https://github.com/actions/toolkit/blob/20ed2908f19538e9dfb66d8083f1171c0a50a87c/packages/glob/src/internal-hash-files.ts#L9-L49
+ * But supports hashing files outside the GITHUB_WORKSPACE.
+ * @param pattern The glob pattern to match files.
+ * @param verbose Whether to log the files being hashed.
+ */
+function hashFiles(pattern_1) {
+    return __awaiter(this, arguments, void 0, function* (pattern, verbose = false) {
+        var _a, e_1, _b, _c;
+        const globber = yield (0, glob_1.create)(pattern);
+        let hasMatch = false;
+        const writeDelegate = verbose ? core.info : core.debug;
+        const result = crypto.createHash("sha256");
+        let count = 0;
+        try {
+            for (var _d = true, _e = __asyncValues(globber.globGenerator()), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
+                _c = _f.value;
+                _d = false;
+                const file = _c;
+                writeDelegate(file);
+                if (fs.statSync(file).isDirectory()) {
+                    writeDelegate(`Skip directory '${file}'.`);
+                    continue;
+                }
+                const hash = crypto.createHash("sha256");
+                const pipeline = util.promisify(stream.pipeline);
+                yield pipeline(fs.createReadStream(file), hash);
+                result.write(hash.digest());
+                count++;
+                if (!hasMatch) {
+                    hasMatch = true;
+                }
+            }
+        }
+        catch (e_1_1) { e_1 = { error: e_1_1 }; }
+        finally {
+            try {
+                if (!_d && !_a && (_b = _e.return)) yield _b.call(_e);
+            }
+            finally { if (e_1) throw e_1.error; }
+        }
+        result.end();
+        if (hasMatch) {
+            writeDelegate(`Found ${count} files to hash.`);
+            return result.digest("hex");
+        }
+        writeDelegate("No matches found for glob");
+        return "";
+    });
+}
+
+
 /***/ }),
 
 /***/ 1653:
@@ -82501,7 +82609,7 @@ exports.githubToken = core.getInput("github-token");
 function getToolBinDir() {
     const toolBinDirInput = core.getInput("tool-bin-dir");
     if (toolBinDirInput !== "") {
-        return toolBinDirInput;
+        return expandTilde(toolBinDirInput);
     }
     if (process.platform === "win32") {
         if (process.env.RUNNER_TEMP !== undefined) {
@@ -82514,7 +82622,7 @@ function getToolBinDir() {
 function getToolDir() {
     const toolDirInput = core.getInput("tool-dir");
     if (toolDirInput !== "") {
-        return toolDirInput;
+        return expandTilde(toolDirInput);
     }
     if (process.platform === "win32") {
         if (process.env.RUNNER_TEMP !== undefined) {
@@ -82527,13 +82635,19 @@ function getToolDir() {
 function getCacheLocalPath() {
     const cacheLocalPathInput = core.getInput("cache-local-path");
     if (cacheLocalPathInput !== "") {
-        return cacheLocalPathInput;
+        return expandTilde(cacheLocalPathInput);
     }
     if (process.env.RUNNER_TEMP !== undefined) {
         return `${process.env.RUNNER_TEMP}${node_path_1.default.sep}setup-uv-cache`;
     }
     throw Error("Could not determine UV_CACHE_DIR. Please make sure RUNNER_TEMP is set or provide the cache-local-path input");
 }
+function expandTilde(input) {
+    if (input.startsWith("~")) {
+        return `${process.env.HOME}${input.substring(1)}`;
+    }
+    return input;
+}
 
 
 /***/ }),
@@ -82684,6 +82798,14 @@ module.exports = require("net");
 
 /***/ }),
 
+/***/ 7598:
+/***/ ((module) => {
+
+"use strict";
+module.exports = require("node:crypto");
+
+/***/ }),
+
 /***/ 8474:
 /***/ ((module) => {
 
@@ -82692,6 +82814,14 @@ module.exports = require("node:events");
 
 /***/ }),
 
+/***/ 3024:
+/***/ ((module) => {
+
+"use strict";
+module.exports = require("node:fs");
+
+/***/ }),
+
 /***/ 6760:
 /***/ ((module) => {
 
diff --git a/dist/setup/index.js b/dist/setup/index.js
index dddf843..516fc7f 100644
--- a/dist/setup/index.js
+++ b/dist/setup/index.js
@@ -87387,10 +87387,10 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
 exports.STATE_CACHE_MATCHED_KEY = exports.STATE_CACHE_KEY = void 0;
 exports.restoreCache = restoreCache;
 const cache = __importStar(__nccwpck_require__(5116));
-const glob = __importStar(__nccwpck_require__(7206));
 const core = __importStar(__nccwpck_require__(7484));
 const inputs_1 = __nccwpck_require__(9612);
 const platforms_1 = __nccwpck_require__(8361);
+const hash_files_1 = __nccwpck_require__(9660);
 exports.STATE_CACHE_KEY = "cache-key";
 exports.STATE_CACHE_MATCHED_KEY = "cache-matched-key";
 const CACHE_VERSION = "1";
@@ -87417,7 +87417,7 @@ function computeKeys(version) {
         let cacheDependencyPathHash = "-";
         if (inputs_1.cacheDependencyGlob !== "") {
             core.info(`Searching files using cache dependency glob: ${inputs_1.cacheDependencyGlob.split("\n").join(",")}`);
-            cacheDependencyPathHash += yield glob.hashFiles(inputs_1.cacheDependencyGlob, undefined, undefined, true);
+            cacheDependencyPathHash += yield (0, hash_files_1.hashFiles)(inputs_1.cacheDependencyGlob, true);
             if (cacheDependencyPathHash === "-") {
                 throw new Error(`No file in ${process.cwd()} matched to [${inputs_1.cacheDependencyGlob.split("\n").join(",")}], make sure you have checked out the target repository`);
             }
@@ -89968,6 +89968,114 @@ function getAvailableVersions(githubToken) {
 }
 
 
+/***/ }),
+
+/***/ 9660:
+/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
+
+"use strict";
+
+var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
+    if (k2 === undefined) k2 = k;
+    var desc = Object.getOwnPropertyDescriptor(m, k);
+    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
+      desc = { enumerable: true, get: function() { return m[k]; } };
+    }
+    Object.defineProperty(o, k2, desc);
+}) : (function(o, m, k, k2) {
+    if (k2 === undefined) k2 = k;
+    o[k2] = m[k];
+}));
+var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
+    Object.defineProperty(o, "default", { enumerable: true, value: v });
+}) : function(o, v) {
+    o["default"] = v;
+});
+var __importStar = (this && this.__importStar) || function (mod) {
+    if (mod && mod.__esModule) return mod;
+    var result = {};
+    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
+    __setModuleDefault(result, mod);
+    return result;
+};
+var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
+    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
+    return new (P || (P = Promise))(function (resolve, reject) {
+        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
+        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
+        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
+        step((generator = generator.apply(thisArg, _arguments || [])).next());
+    });
+};
+var __asyncValues = (this && this.__asyncValues) || function (o) {
+    if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
+    var m = o[Symbol.asyncIterator], i;
+    return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
+    function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
+    function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
+};
+Object.defineProperty(exports, "__esModule", ({ value: true }));
+exports.hashFiles = hashFiles;
+const crypto = __importStar(__nccwpck_require__(7598));
+const core = __importStar(__nccwpck_require__(7484));
+const fs = __importStar(__nccwpck_require__(3024));
+const stream = __importStar(__nccwpck_require__(7075));
+const util = __importStar(__nccwpck_require__(7975));
+const glob_1 = __nccwpck_require__(7206);
+/**
+ * Hashes files matching the given glob pattern.
+ *
+ * Copied from https://github.com/actions/toolkit/blob/20ed2908f19538e9dfb66d8083f1171c0a50a87c/packages/glob/src/internal-hash-files.ts#L9-L49
+ * But supports hashing files outside the GITHUB_WORKSPACE.
+ * @param pattern The glob pattern to match files.
+ * @param verbose Whether to log the files being hashed.
+ */
+function hashFiles(pattern_1) {
+    return __awaiter(this, arguments, void 0, function* (pattern, verbose = false) {
+        var _a, e_1, _b, _c;
+        const globber = yield (0, glob_1.create)(pattern);
+        let hasMatch = false;
+        const writeDelegate = verbose ? core.info : core.debug;
+        const result = crypto.createHash("sha256");
+        let count = 0;
+        try {
+            for (var _d = true, _e = __asyncValues(globber.globGenerator()), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
+                _c = _f.value;
+                _d = false;
+                const file = _c;
+                writeDelegate(file);
+                if (fs.statSync(file).isDirectory()) {
+                    writeDelegate(`Skip directory '${file}'.`);
+                    continue;
+                }
+                const hash = crypto.createHash("sha256");
+                const pipeline = util.promisify(stream.pipeline);
+                yield pipeline(fs.createReadStream(file), hash);
+                result.write(hash.digest());
+                count++;
+                if (!hasMatch) {
+                    hasMatch = true;
+                }
+            }
+        }
+        catch (e_1_1) { e_1 = { error: e_1_1 }; }
+        finally {
+            try {
+                if (!_d && !_a && (_b = _e.return)) yield _b.call(_e);
+            }
+            finally { if (e_1) throw e_1.error; }
+        }
+        result.end();
+        if (hasMatch) {
+            writeDelegate(`Found ${count} files to hash.`);
+            return result.digest("hex");
+        }
+        writeDelegate("No matches found for glob");
+        return "";
+    });
+}
+
+
 /***/ }),
 
 /***/ 2180:
@@ -90176,7 +90284,7 @@ exports.githubToken = core.getInput("github-token");
 function getToolBinDir() {
     const toolBinDirInput = core.getInput("tool-bin-dir");
     if (toolBinDirInput !== "") {
-        return toolBinDirInput;
+        return expandTilde(toolBinDirInput);
     }
     if (process.platform === "win32") {
         if (process.env.RUNNER_TEMP !== undefined) {
@@ -90189,7 +90297,7 @@ function getToolBinDir() {
 function getToolDir() {
     const toolDirInput = core.getInput("tool-dir");
     if (toolDirInput !== "") {
-        return toolDirInput;
+        return expandTilde(toolDirInput);
     }
     if (process.platform === "win32") {
         if (process.env.RUNNER_TEMP !== undefined) {
@@ -90202,13 +90310,19 @@ function getToolDir() {
 function getCacheLocalPath() {
     const cacheLocalPathInput = core.getInput("cache-local-path");
     if (cacheLocalPathInput !== "") {
-        return cacheLocalPathInput;
+        return expandTilde(cacheLocalPathInput);
     }
     if (process.env.RUNNER_TEMP !== undefined) {
         return `${process.env.RUNNER_TEMP}${node_path_1.default.sep}setup-uv-cache`;
     }
     throw Error("Could not determine UV_CACHE_DIR. Please make sure RUNNER_TEMP is set or provide the cache-local-path input");
 }
+function expandTilde(input) {
+    if (input.startsWith("~")) {
+        return `${process.env.HOME}${input.substring(1)}`;
+    }
+    return input;
+}
 
 
 /***/ }),
diff --git a/src/cache/restore-cache.ts b/src/cache/restore-cache.ts
index 0e95343..0c07873 100644
--- a/src/cache/restore-cache.ts
+++ b/src/cache/restore-cache.ts
@@ -1,5 +1,4 @@
 import * as cache from "@actions/cache";
-import * as glob from "@actions/glob";
 import * as core from "@actions/core";
 import {
   cacheDependencyGlob,
@@ -7,6 +6,7 @@ import {
   cacheSuffix,
 } from "../utils/inputs";
 import { getArch, getPlatform } from "../utils/platforms";
+import { hashFiles } from "../hash/hash-files";
 
 export const STATE_CACHE_KEY = "cache-key";
 export const STATE_CACHE_MATCHED_KEY = "cache-matched-key";
@@ -39,12 +39,7 @@ async function computeKeys(version: string): Promise<string> {
     core.info(
       `Searching files using cache dependency glob: ${cacheDependencyGlob.split("\n").join(",")}`,
     );
-    cacheDependencyPathHash += await glob.hashFiles(
-      cacheDependencyGlob,
-      undefined,
-      undefined,
-      true,
-    );
+    cacheDependencyPathHash += await hashFiles(cacheDependencyGlob, true);
     if (cacheDependencyPathHash === "-") {
       throw new Error(
         `No file in ${process.cwd()} matched to [${cacheDependencyGlob.split("\n").join(",")}], make sure you have checked out the target repository`,
diff --git a/src/hash/hash-files.ts b/src/hash/hash-files.ts
new file mode 100644
index 0000000..87f852b
--- /dev/null
+++ b/src/hash/hash-files.ts
@@ -0,0 +1,48 @@
+import * as crypto from "node:crypto";
+import * as core from "@actions/core";
+import * as fs from "node:fs";
+import * as stream from "node:stream";
+import * as util from "node:util";
+import { create } from "@actions/glob";
+
+/**
+ * Hashes files matching the given glob pattern.
+ *
+ * Copied from https://github.com/actions/toolkit/blob/20ed2908f19538e9dfb66d8083f1171c0a50a87c/packages/glob/src/internal-hash-files.ts#L9-L49
+ * But supports hashing files outside the GITHUB_WORKSPACE.
+ * @param pattern The glob pattern to match files.
+ * @param verbose Whether to log the files being hashed.
+ */
+export async function hashFiles(
+  pattern: string,
+  verbose = false,
+): Promise<string> {
+  const globber = await create(pattern);
+  let hasMatch = false;
+  const writeDelegate = verbose ? core.info : core.debug;
+  const result = crypto.createHash("sha256");
+  let count = 0;
+  for await (const file of globber.globGenerator()) {
+    writeDelegate(file);
+    if (fs.statSync(file).isDirectory()) {
+      writeDelegate(`Skip directory '${file}'.`);
+      continue;
+    }
+    const hash = crypto.createHash("sha256");
+    const pipeline = util.promisify(stream.pipeline);
+    await pipeline(fs.createReadStream(file), hash);
+    result.write(hash.digest());
+    count++;
+    if (!hasMatch) {
+      hasMatch = true;
+    }
+  }
+  result.end();
+
+  if (hasMatch) {
+    writeDelegate(`Found ${count} files to hash.`);
+    return result.digest("hex");
+  }
+  writeDelegate("No matches found for glob");
+  return "";
+}
diff --git a/src/utils/inputs.ts b/src/utils/inputs.ts
index f010106..57af924 100644
--- a/src/utils/inputs.ts
+++ b/src/utils/inputs.ts
@@ -15,7 +15,7 @@ export const githubToken = core.getInput("github-token");
 function getToolBinDir(): string | undefined {
   const toolBinDirInput = core.getInput("tool-bin-dir");
   if (toolBinDirInput !== "") {
-    return toolBinDirInput;
+    return expandTilde(toolBinDirInput);
   }
   if (process.platform === "win32") {
     if (process.env.RUNNER_TEMP !== undefined) {
@@ -31,7 +31,7 @@ function getToolBinDir(): string | undefined {
 function getToolDir(): string | undefined {
   const toolDirInput = core.getInput("tool-dir");
   if (toolDirInput !== "") {
-    return toolDirInput;
+    return expandTilde(toolDirInput);
   }
   if (process.platform === "win32") {
     if (process.env.RUNNER_TEMP !== undefined) {
@@ -47,7 +47,7 @@ function getToolDir(): string | undefined {
 function getCacheLocalPath(): string {
   const cacheLocalPathInput = core.getInput("cache-local-path");
   if (cacheLocalPathInput !== "") {
-    return cacheLocalPathInput;
+    return expandTilde(cacheLocalPathInput);
   }
   if (process.env.RUNNER_TEMP !== undefined) {
     return `${process.env.RUNNER_TEMP}${path.sep}setup-uv-cache`;
@@ -56,3 +56,10 @@ function getCacheLocalPath(): string {
     "Could not determine UV_CACHE_DIR. Please make sure RUNNER_TEMP is set or provide the cache-local-path input",
   );
 }
+
+function expandTilde(input: string): string {
+  if (input.startsWith("~")) {
+    return `${process.env.HOME}${input.substring(1)}`;
+  }
+  return input;
+}