API Docs

Module for filesystem operations.

hs.fs provides a comprehensive set of filesystem operations covering file I/O, directory management, path manipulation, metadata access, symbolic links, Finder tags, and macOS-specific features like file bookmarks and Uniform Type Identifiers.

It replaces both Hammerspoon v1's hs.fs module and the functionality that was previously available through Lua's built-in io and file modules.

Reading and writing files

const contents = hs.fs.read("/etc/hosts");           // entire file
const chunk    = hs.fs.read("/etc/hosts", 100, 50);  // 50 bytes from offset 100

hs.fs.eachLine("/etc/hosts", function(line) {
    console.log(line);
    // return false to stop early
});

hs.fs.write("/tmp/hello.txt", "Hello, world!\n");
hs.fs.append("/tmp/hello.txt", "More content\n");

Working with open files

For incremental, positioned, binary, or locked access, open the file to get an HSFile object (the equivalent of a Lua io file handle):

const f = hs.fs.open("~/notes.txt", "r+");
const firstLine = f.readLine();
f.seek(0, "end");
f.writeLine("appended");
f.close();

// Or let hs.fs.withFile() close it for you:
const header = hs.fs.withFile("~/image.png", "r", f => f.readBytes(8));

Errors

Functions that fail return null or false and set hs.fs.lastError to an object like {code: "ENOENT", message: "No such file or directory"}. Most failures are also logged to the Console; failures that are often expected (such as hs.fs.open() on a missing file) are not.

Directory operations

hs.fs.mkdir("~/Projects/new-thing");

const files = hs.fs.list("~/Documents");
const all   = hs.fs.listRecursive("~/Documents");

Path utilities

const abs  = hs.fs.pathToAbsolute("~/Library");
const tmp  = hs.fs.tempDirectory();
const home = hs.fs.homeDirectory();

Metadata

const info = hs.fs.attributes("/etc/hosts");
// { size: 1234, type: "file", permissions: 420,
//   ownerID: 0, groupID: 0,
//   creationDate: 1700000000.0, modificationDate: 1700001000.0 }

Types

This module provides the following types:

Properties

hs.fs.lastError

JSValue
The error from the most recent failed `hs.fs` function, or `null` if none has failed. The object has a `code` (a POSIX error name such as `"ENOENT"`, `"EEXIST"`, or `"EACCES"`, or `"EINVAL"` for invalid arguments) and a human-readable `message`. Like C's `errno`, it is not cleared by successful calls, so only consult it after a function has reported failure. Errors from methods on an open file are recorded on that file's own `lastError` instead.

Methods

hs.fs.open(path, mode, permissions) -> HSFile | null

Open a file, returning an `HSFile` object for reading and/or writing it. | Mode | Read | Write | Creates | Truncates | Notes | |------|------|-------|---------|-----------|-------| | `"r"` | ✓ | | | | The default | | `"r+"` | ✓ | ✓ | | | | | `"w"` | | ✓ | ✓ | ✓ | | | `"w+"` | ✓ | ✓ | ✓ | ✓ | | | `"a"` | | ✓ | ✓ | | Every write goes to the end of the file | | `"a+"` | ✓ | ✓ | ✓ | | Reads start at the beginning; every write goes to the end | Add `x` to a `w` mode (`"wx"`, `"w+x"`) to fail if the file already exists. A `b` is accepted and ignored, since all files can be read as text or bytes. Directories cannot be opened. A file that can't be opened (for example because it doesn't exist) is not logged to the Console, since that is often an expected outcome; check `hs.fs.lastError` instead. Invalid arguments, such as an unknown mode, are logged. Close the file with `close()` when you are finished with it; any files still open are closed automatically when your configuration is reloaded.
hs.fs.open(path, mode, permissions) -> HSFile | null
Name Type Description
path string Path to the file. `~` is expanded.
mode string The open mode. Defaults to `"r"`.
permissions number POSIX permission bits applied if the file is created, e.g. `0o600` (`0` creates a file with no permissions). Defaults to `0o644` when omitted; any other non-integer value, including `NaN`, fails with `EINVAL`. The process umask is applied in either case.
HSFile | null
An `HSFile`, or `null` on failure (see `hs.fs.lastError`).
const f = hs.fs.open("~/notes.txt", "a")
if (f) {
    f.writeLine("Another note")
    f.close()
}

hs.fs.tempFile(prefix) -> HSFile | null

Create and open a new, uniquely named temporary file. The file is created in `hs.fs.tempDirectory()` with permissions `0o600` and opened in `"w+"` mode, so its `path` always starts with `hs.fs.tempDirectory()`. It is not deleted automatically; call `remove()` on it when you no longer need it on disk.
hs.fs.tempFile(prefix) -> HSFile | null
Name Type Description
prefix string A prefix for the file name. Must not contain `/`. Defaults to `"hs"`.
HSFile | null
An `HSFile`, or `null` on failure (see `hs.fs.lastError`).
const tmp = hs.fs.tempFile("myspoon")
tmp.write("scratch data")
console.log("Wrote " + tmp.path)

hs.fs.withFile(path, mode, callback) -> any

Open a file, pass it to a function, and close it again afterwards. The file is closed when the function returns, even if it throws (in which case the exception propagates to you). The function must do all its work synchronously: an `async` function would find the file already closed after its first `await`. `hs.fs.withFile(path, f => ...)`.
hs.fs.withFile(path, mode, callback) -> any
Name Type Description
path string Path to the file. `~` is expanded.
mode string The open mode, as for `hs.fs.open()`. Defaults to `"r"`; if you omit it, pass the callback in its place.
callback function Called with the open file. Required unless it was passed in place of `mode`.
any
Whatever `callback` returns, or `null` if the file could not be opened or no callback was given (see `hs.fs.lastError`).
const header = hs.fs.withFile("/etc/hosts", f => f.readLine())

hs.fs.withFile("~/counter.txt", "r+", f => {
    const n = (parseInt(f.read()) || 0) + 1
    f.truncate()
    f.rewind()
    f.write(String(n))
})

hs.fs.read(path, offset, length) -> string

Read part or all of a file as a UTF-8 string.
hs.fs.read(path, offset, length) -> string
Name Type Description
path string Path to the file. `~` is expanded.
offset number Byte offset to start reading from. Pass `0` (or omit) to read from the beginning.
length number Maximum number of bytes to read. Pass `0` (or omit) to read to the end of the file.
string
The file contents as a UTF-8 string, or `null` if the file cannot be read.
const all   = hs.fs.read("/etc/hosts")            // entire file
const chunk = hs.fs.read("/etc/hosts", 100, 50)   // 50 bytes starting at byte 100

hs.fs.eachLine(path, callback) -> boolean

Call a function for each line of a file. This behaves exactly like opening the file and calling `HSFile.eachLine()`: line endings (`\n` or `\r\n`) are stripped; the callback may return `false` (and only `false`) to stop early; if the callback throws, iteration stops and the exception propagates to you; and invalid UTF-8 stops iteration with `hs.fs.lastError.code === "EILSEQ"`.
hs.fs.eachLine(path, callback) -> boolean
Name Type Description
path string Path to the file. `~` is expanded.
callback function Called once per line. Return `false` to stop.
boolean
`true` if iteration completed or was stopped by the callback returning `false`; `false` if the file could not be opened or read (see `hs.fs.lastError`), or if the callback threw.
hs.fs.eachLine("/etc/hosts", (line) => {
    if (line.startsWith("#")) return
    console.log(line)
})

hs.fs.write(path, content, inPlace) -> boolean

Write a UTF-8 string to a file, creating it or overwriting any existing content. Intermediate directories are not created automatically; use `mkdir` first if needed.
hs.fs.write(path, content, inPlace) -> boolean
Name Type Description
path string Path to the file. `~` is expanded.
content string String to write.
inPlace boolean Whether to write the file in-place or atomically. Defaults to atomically (false).
boolean
`true` on success, `false` on failure.
hs.fs.write("/tmp/hello.txt", "Hello, world!\n")

hs.fs.append(path, content) -> boolean

Append a UTF-8 string to a file, creating it if it does not exist.
hs.fs.append(path, content) -> boolean
Name Type Description
path string Path to the file. `~` is expanded.
content string String to append.
boolean
`true` on success, `false` on failure.
hs.fs.append("/tmp/log.txt", "another line\n")

hs.fs.exists(path) -> boolean

Determine if a filesystem object exists at the given path Unlike `isFile` and `isDirectory`, this follows symlinks.
hs.fs.exists(path) -> boolean
Name Type Description
path string Path to check. `~` is expanded.
boolean
`true` if any filesystem entry (file, directory, symlink, etc.) exists at the path.
if (hs.fs.exists("/tmp/file.txt")) console.log("exists")

hs.fs.isFile(path) -> boolean

Determine if a file exists at the given path This does **not** follow symlinks; a symlink pointing at a file returns `false`.
hs.fs.isFile(path) -> boolean
Name Type Description
path string Path to check. `~` is expanded.
boolean
`true` if a regular file (not a directory or symlink) exists at the path.
console.log(hs.fs.isFile("/etc/hosts"))

hs.fs.isDirectory(path) -> boolean

Determine if a directory exists at the given path This does **not** follow symlinks; a symlink pointing at a directory returns `false`.
hs.fs.isDirectory(path) -> boolean
Name Type Description
path string Path to check. `~` is expanded.
boolean
`true` if a directory exists at the path.
console.log(hs.fs.isDirectory("/tmp"))

hs.fs.isReadable(path) -> boolean

Determine if a given filesystem path is readable
hs.fs.isReadable(path) -> boolean
Name Type Description
path string Path to check. `~` is expanded.
boolean
`true` if the current process can read the file or directory at the path.
console.log(hs.fs.isReadable("/etc/hosts"))

hs.fs.isWritable(path) -> boolean

Determine if a given filesystem path is writable
hs.fs.isWritable(path) -> boolean
Name Type Description
path string Path to check. `~` is expanded.
boolean
`true` if the current process can write to the file or directory at the path.
console.log(hs.fs.isWritable("/tmp"))

hs.fs.copy(source, destination) -> boolean

Copy a file or directory to a new location. The destination must not already exist. If `source` is a directory, its entire contents are copied recursively.
hs.fs.copy(source, destination) -> boolean
Name Type Description
source string Path to the existing file or directory. `~` is expanded.
destination string Path for the copy. `~` is expanded.
boolean
`true` on success, `false` on failure.
hs.fs.copy("/tmp/a.txt", "/tmp/b.txt")

hs.fs.move(source, destination) -> boolean

Move (rename) a file or directory. The destination must not already exist.
hs.fs.move(source, destination) -> boolean
Name Type Description
source string Path to the existing file or directory. `~` is expanded.
destination string New path. `~` is expanded.
boolean
`true` on success, `false` on failure.
hs.fs.move("/tmp/old.txt", "/tmp/new.txt")

hs.fs.deletePath(path) -> boolean

Delete a file or directory at the given path. Directories are removed recursively. To remove only an empty directory, use `rmdir` instead.
hs.fs.deletePath(path) -> boolean
Name Type Description
path string Path to delete. `~` is expanded.
boolean
`true` on success, `false` on failure.
hs.fs.deletePath("/tmp/old.txt")

hs.fs.list(path) -> [String]

List the immediate contents of a directory. Returns bare filenames (not full paths), sorted alphabetically. The `.` and `..` entries are never included.
hs.fs.list(path) -> [String]
Name Type Description
path string Path to the directory. `~` is expanded.
[String]
Sorted array of filenames, or `null` if the path cannot be read.
const files = hs.fs.list("~/Documents")

hs.fs.listRecursive(path) -> [String]

Recursively list all entries under a directory. Returns paths relative to `path`, sorted alphabetically.
hs.fs.listRecursive(path) -> [String]
Name Type Description
path string Path to the root directory. `~` is expanded.
[String]
Sorted array of relative paths, or `null` if the path cannot be read.
const all = hs.fs.listRecursive("~/Documents")

hs.fs.mkdir(path) -> boolean

Create a directory, including all necessary intermediate directories. Succeeds silently if the directory already exists.
hs.fs.mkdir(path) -> boolean
Name Type Description
path string Path of the directory to create. `~` is expanded.
boolean
`true` on success, `false` on failure.
hs.fs.mkdir("~/Projects/new-thing")

hs.fs.rmdir(path) -> boolean

Remove an empty directory. Fails if the directory is not empty. Use `deletePath` to remove a non-empty directory recursively.
hs.fs.rmdir(path) -> boolean
Name Type Description
path string Path of the directory to remove. `~` is expanded.
boolean
`true` on success, `false` on failure.
hs.fs.rmdir("/tmp/empty-dir")

hs.fs.currentDir() -> string

Returns the current working directory of the process.
hs.fs.currentDir() -> string
string
Current directory path, or `null` on error.
console.log(hs.fs.currentDir())

hs.fs.chdir(path) -> boolean

Change the current working directory of the process.
hs.fs.chdir(path) -> boolean
Name Type Description
path string New working directory path. `~` is expanded.
boolean
`true` on success, `false` on failure.
hs.fs.chdir("~/Projects")

hs.fs.pathToAbsolute(path) -> string

Resolve a path to its absolute, canonical form. Expands `~`, resolves `.` and `..`, and follows all symbolic links. Returns `null` if any component of the path does not exist.
hs.fs.pathToAbsolute(path) -> string
Name Type Description
path string Path to resolve.
string
Absolute canonical path, or `null` if it cannot be resolved.
console.log(hs.fs.pathToAbsolute("~/Library"))

hs.fs.displayName(path) -> string

Return the localised display name for a file or directory as shown by Finder. For example, `/Library` appears as `"Library"` in Finder even though its on-disk name is the same.
hs.fs.displayName(path) -> string
Name Type Description
path string Path to the file or directory. `~` is expanded.
string
Display name string, or `null` if the path does not exist.
console.log(hs.fs.displayName("/Library"))

hs.fs.tempDirectory() -> string

Returns the temporary directory for the current user. The path is fully resolved (e.g. `/private/var/folders/...` rather than the `/var/folders/...` symlink), so it matches the `path` of files created by `hs.fs.tempFile()`.
hs.fs.tempDirectory() -> string
string
Absolute temporary directory path, with symlinks resolved (always ends with `/`).
console.log(hs.fs.tempDirectory())

hs.fs.homeDirectory() -> string

Returns the home directory for the current user.
hs.fs.homeDirectory() -> string
string
Home directory path string.
console.log(hs.fs.homeDirectory())

hs.fs.urlFromPath(path) -> string

Returns a `file://` URL string for the given path.
hs.fs.urlFromPath(path) -> string
Name Type Description
path string Filesystem path. `~` is expanded.
string
URL string
console.log(hs.fs.urlFromPath("/tmp/foo.txt"))
// → "file:///tmp/foo.txt"

hs.fs.attributes(path) -> [String: Any]

Get metadata attributes for a file or directory. Does not follow symbolic links. Use `isSymlink` to detect links before calling this if needed.
hs.fs.attributes(path) -> [String: Any]
Name Type Description
path string Path to inspect. `~` is expanded.
[String: Any]
Attributes object, or `null` if the path cannot be accessed.
const info = hs.fs.attributes("/etc/hosts")
console.log(info.size, info.type)

hs.fs.touch(path, accessDate, modificationDate) -> boolean

Set the access and modification times of a file, creating it if it does not exist (equivalent to the POSIX `touch` command). The argument order matches Hammerspoon v1 (LuaFileSystem) and Node's `fs.utimes`: access time first.
hs.fs.touch(path, accessDate, modificationDate) -> boolean
Name Type Description
path string Path to the file. `~` is expanded.
accessDate number Seconds since the Unix epoch (fractions allowed). Defaults to now.
modificationDate number Seconds since the Unix epoch (fractions allowed). Defaults to `accessDate`.
boolean
`true` on success, `false` on failure (including `EINVAL` for a `NaN`, infinite, or out-of-range timestamp).
hs.fs.touch("/tmp/marker.txt")                             // now
hs.fs.touch("/tmp/marker.txt", Date.now() / 1000 - 86400)  // both times one day ago

hs.fs.setPermissions(path, permissions) -> boolean

Set the POSIX permission bits of a file or directory. Follows symbolic links.
hs.fs.setPermissions(path, permissions) -> boolean
Name Type Description
path string Path to the file or directory. `~` is expanded.
permissions number The permission bits, as an integer between `0` and `0o7777`, e.g. `0o644`. Use an octal literal: decimal `755` is a different (and unusual) mode.
boolean
`true` on success, `false` on failure (including `EINVAL` if `permissions` is missing or out of range).
hs.fs.setPermissions("~/bin/myscript.sh", 0o755)

hs.fs.tags(path) -> [String]

Get the Finder tags assigned to a file or directory.
hs.fs.tags(path) -> [String]
Name Type Description
path string Path to the file or directory. `~` is expanded.
[String]
Array of tag name strings, or `null` if no tags are set.
console.log(hs.fs.tags("~/Documents/report.pdf"))

hs.fs.fileUTI(path) -> string

Replace all Finder tags on a file or directory. This function is only available on macOS Tahoe (26) or later.
hs.fs.fileUTI(path) -> string
Name Type Description
path string Path to the file.
string
`true` on success, `false` on failure.
hs.fs.setTags("~/Documents/report.pdf", ["Important", "Work"])
hs.fs.addTags("~/Documents/report.pdf", ["Reviewed"])
hs.fs.removeTags("~/Documents/report.pdf", ["Draft"])
console.log(hs.fs.fileUTI("/etc/hosts"))    // → "public.plain-text"
console.log(hs.fs.fileUTI("/tmp/foo.png"))  // → "public.png"

hs.fs.pathToBookmark(path) -> string

Encode a file path as a persistent bookmark that survives file moves and renames. The returned string is base64-encoded bookmark data that can be stored and later resolved with `pathFromBookmark`.
hs.fs.pathToBookmark(path) -> string
Name Type Description
path string Path to the file or directory. `~` is expanded.
string
Base64-encoded bookmark string, or `null` on failure.
const data = hs.fs.pathToBookmark("/tmp/foo.txt")

hs.fs.pathFromBookmark(data) -> string

Resolve a base64-encoded bookmark back to a file path.
hs.fs.pathFromBookmark(data) -> string
Name Type Description
data string Base64-encoded bookmark string produced by `pathToBookmark`.
string
The current file path, or `null` if the bookmark cannot be resolved.
const path = hs.fs.pathFromBookmark(savedData)

hs.fs.volumes(showHidden) -> [String: Any]

Return information about all currently mounted filesystem volumes.
hs.fs.volumes(showHidden) -> [String: Any]
Name Type Description
showHidden boolean Pass `true` to include hidden volumes. Defaults to `false`.
[String: Any]
Object keyed by mount path, or `null` on failure.
const vols = hs.fs.volumes()
for (const [path, info] of Object.entries(vols)) {
    console.log(path + " — " + info.name)
}

hs.fs.ejectVolume(path) -> boolean

Unmount and eject the volume at the given path.
hs.fs.ejectVolume(path) -> boolean
Name Type Description
path string The mount path of the volume to eject. `~` is expanded.
boolean
`true` if the volume was ejected successfully, `false` otherwise.
const ok = hs.fs.ejectVolume("/Volumes/MyDisk")

hs.fs.addVolumeWatcher() -> HSVolumeWatcher

Create a new volume event watcher. Call `setCallback()` and `start()` on the returned object to begin receiving volume mount/unmount/rename events.
hs.fs.addVolumeWatcher() -> HSVolumeWatcher
HSVolumeWatcher
An `HSVolumeWatcher` object.
const w = hs.fs.addVolumeWatcher()
w.setCallback((event, info) => {
    console.log(event + ": " + info.path)
}).start()

hs.fs.removeVolumeWatcher(watcher) -> None

Stop and destroy a volume watcher previously created with `addVolumeWatcher`.
hs.fs.removeVolumeWatcher(watcher) -> None
Name Type Description
watcher HSVolumeWatcher The watcher to remove.
None
const w = hs.fs.addVolumeWatcher()
// ... later ...
hs.fs.removeVolumeWatcher(w)

hs.fs.createPathWatcher(path) -> HSPathWatcher

Create a watcher for filesystem events at a given path. Events are batched and delivered with a latency of approximately one second. Call `setCallback()` and `start()` on the returned object to begin receiving events.
hs.fs.createPathWatcher(path) -> HSPathWatcher
Name Type Description
path string The path to watch. `~` is expanded.
HSPathWatcher
An `HSPathWatcher` object.
const w = hs.fs.createPathWatcher("/Users/me/Documents")
w.setCallback((paths, flags) => {
    paths.forEach((p, i) => console.log(flags[i].join(",") + ": " + p))
}).start()

hs.fs.xattrGet(path, attribute, options, position) -> string

Get the value of an extended attribute for a file or directory. Attribute values are returned as ISO Latin-1 encoded strings so that arbitrary byte sequences are represented without loss. ASCII text attribute values appear readable as-is.
hs.fs.xattrGet(path, attribute, options, position) -> string
Name Type Description
path string Path to the file or directory. `~` is expanded.
attribute string Name of the extended attribute.
options NSArray Array of option strings: `"noFollow"` (do not follow symlinks), `"hfsCompression"`, `"createOnly"`, `"replaceOnly"`, `"noSecurity"`, `"noDefault"`. Pass an empty array or omit to use no options.
position number Byte offset within the attribute data. Defaults to `0`. Non-zero values are only valid for `"com.apple.ResourceFork"`.
string
The attribute value as a string, `""` if the attribute exists but contains no data, or `null` if the attribute does not exist or an error occurs.
const quarantine = hs.fs.xattrGet("/path/to/file.dmg", "com.apple.quarantine")
if (quarantine !== null) console.log("quarantine: " + quarantine)

hs.fs.xattrList(path, options) -> [String]

List all extended attributes defined for a file or directory.
hs.fs.xattrList(path, options) -> [String]
Name Type Description
path string Path to the file or directory. `~` is expanded.
options NSArray Array of option strings. Pass an empty array or omit to use no options.
[String]
Array of attribute name strings (may be empty), or `null` on error.
const attrs = hs.fs.xattrList("/path/to/file.dmg")
if (attrs) attrs.forEach(a => console.log(a))

hs.fs.xattrSet(path, attribute, value, options, position) -> boolean

Set the value of an extended attribute for a file or directory. The value is written as ISO Latin-1 bytes, providing a lossless round-trip with `xattrGet`. Plain ASCII strings work directly without any encoding.
hs.fs.xattrSet(path, attribute, value, options, position) -> boolean
Name Type Description
path string Path to the file or directory. `~` is expanded.
attribute string Name of the extended attribute.
value string The value to write.
options NSArray Array of option strings: `"noFollow"`, `"hfsCompression"`, `"createOnly"`, `"replaceOnly"`, `"noSecurity"`, `"noDefault"`. Pass an empty array or omit to use no options.
position number Byte offset within the attribute data. Defaults to `0`. Non-zero values are only valid for `"com.apple.ResourceFork"`.
boolean
`true` on success, `false` on failure.
hs.fs.xattrSet("/path/to/file.txt", "com.example.origin", "https://example.com")

hs.fs.xattrRemove(path, attribute, options) -> boolean

Remove an extended attribute from a file or directory.
hs.fs.xattrRemove(path, attribute, options) -> boolean
Name Type Description
path string Path to the file or directory. `~` is expanded.
attribute string Name of the extended attribute to remove.
options NSArray Array of option strings: `"noFollow"`, `"hfsCompression"`. Pass an empty array or omit to use no options.
boolean
`true` on success, `false` on failure (including if the attribute does not exist).
hs.fs.xattrRemove("/path/to/file.dmg", "com.apple.quarantine")