repos / zmx

session persistence for terminal processes
git clone https://github.com/neurosnap/zmx.git

commit
fbf0171
parent
5f30ba8
author
Adrian
date
2026-04-13 11:58:12 -0400 EDT
feat: allow configuring socket and log permissions via environment variables (#130)
3 files changed,  +67, -7
M README.md
+9, -0
 1@@ -360,6 +360,15 @@ Each session gets its own unix socket file. The default location depends on your
 2 1. `TMPDIR` => uses `{TMPDIR}/zmx-{uid}` (appends uid for multi-user safety)
 3 1. `/tmp` => uses `/tmp/zmx-{uid}` (default fallback, appends uid for multi-user safety)
 4 
 5+## permissions
 6+
 7+You can configure the permissions for the socket directory and log files using the following environment variables:
 8+
 9+- `ZMX_DIR_MODE` => sets the mode for the socket and log directories (octal, defaults to `0750`)
10+- `ZMX_LOG_MODE` => sets the mode for the log files (octal, defaults to `0640`)
11+
12+This is particularly useful when running `zmx` as a system service with a shared group. For example, setting `ZMX_DIR_MODE=0770` and `ZMX_LOG_MODE=0660` allows group members to attach to the session.
13+
14 ## debugging
15 
16 We store global logs for cli commands in `{socket_dir}/logs/zmx.log`. We store session-specific logs in `{socket_dir}/logs/{session_name}.log`. Right now they are enabled by default and cannot be disabled. The idea here is to help with initial development until we reach a stable state.
M src/log.zig
+5, -3
 1@@ -7,15 +7,17 @@ pub const LogSystem = struct {
 2     max_size: u64 = 5 * 1024 * 1024, // 5MB
 3     path: []const u8 = "",
 4     alloc: std.mem.Allocator = undefined,
 5+    mode: u32 = 0o640,
 6 
 7-    pub fn init(self: *LogSystem, alloc: std.mem.Allocator, path: []const u8) !void {
 8+    pub fn init(self: *LogSystem, alloc: std.mem.Allocator, path: []const u8, mode: u32) !void {
 9         self.alloc = alloc;
10         self.path = try alloc.dupe(u8, path);
11+        self.mode = mode;
12 
13         const file = std.fs.openFileAbsolute(path, .{ .mode = .read_write }) catch |err| switch (err) {
14             error.FileNotFound => try std.fs.createFileAbsolute(
15                 path,
16-                .{ .read = true, .mode = 0o640 },
17+                .{ .read = true, .mode = @intCast(self.mode) },
18             ),
19             else => return err,
20         };
21@@ -93,7 +95,7 @@ pub const LogSystem = struct {
22 
23         self.file = try std.fs.createFileAbsolute(
24             self.path,
25-            .{ .truncate = true, .read = true, .mode = 0o640 },
26+            .{ .truncate = true, .read = true, .mode = @intCast(self.mode) },
27         );
28         self.current_size = 0;
29     }
M src/main.zig
+53, -4
  1@@ -55,7 +55,7 @@ pub fn main() !void {
  2 
  3     const log_path = try std.fs.path.join(alloc, &.{ cfg.log_dir, "zmx.log" });
  4     defer alloc.free(log_path);
  5-    try log_system.init(alloc, log_path);
  6+    try log_system.init(alloc, log_path, cfg.log_mode);
  7     defer log_system.deinit();
  8 
  9     const cmd = args.next() orelse {
 10@@ -270,15 +270,29 @@ const Cfg = struct {
 11     socket_dir: []const u8,
 12     log_dir: []const u8,
 13     max_scrollback: usize = 10_000_000,
 14+    dir_mode: u32 = 0o750,
 15+    log_mode: u32 = 0o640,
 16 
 17     pub fn init(alloc: std.mem.Allocator) !Cfg {
 18         const socket_dir = try socketDir(alloc);
 19         const log_dir = try std.fmt.allocPrint(alloc, "{s}/logs", .{socket_dir});
 20         errdefer alloc.free(log_dir);
 21 
 22+        const dir_mode = if (std.posix.getenv("ZMX_DIR_MODE")) |m|
 23+            std.fmt.parseInt(u32, m, 8) catch 0o750
 24+        else
 25+            0o750;
 26+
 27+        const log_mode = if (std.posix.getenv("ZMX_LOG_MODE")) |m|
 28+            std.fmt.parseInt(u32, m, 8) catch 0o640
 29+        else
 30+            0o640;
 31+
 32         var cfg = Cfg{
 33             .socket_dir = socket_dir,
 34             .log_dir = log_dir,
 35+            .dir_mode = dir_mode,
 36+            .log_mode = log_mode,
 37         };
 38 
 39         try cfg.mkdir();
 40@@ -307,18 +321,51 @@ const Cfg = struct {
 41     }
 42 
 43     pub fn mkdir(self: *Cfg) !void {
 44-        posix.mkdirat(posix.AT.FDCWD, self.socket_dir, 0o750) catch |err| switch (err) {
 45+        posix.mkdirat(posix.AT.FDCWD, self.socket_dir, @intCast(self.dir_mode)) catch |err| switch (err) {
 46             error.PathAlreadyExists => {},
 47             else => return err,
 48         };
 49 
 50-        posix.mkdirat(posix.AT.FDCWD, self.log_dir, 0o750) catch |err| switch (err) {
 51+        posix.mkdirat(posix.AT.FDCWD, self.log_dir, @intCast(self.dir_mode)) catch |err| switch (err) {
 52             error.PathAlreadyExists => {},
 53             else => return err,
 54         };
 55     }
 56 };
 57 
 58+test "Cfg.init uses default modes when env vars are not set" {
 59+    const alloc = std.testing.allocator;
 60+
 61+    // Ensure they are not set
 62+    _ = cross.c.unsetenv("ZMX_DIR_MODE");
 63+    _ = cross.c.unsetenv("ZMX_LOG_MODE");
 64+
 65+    var cfg = try Cfg.init(alloc);
 66+    defer cfg.deinit(alloc);
 67+
 68+    try std.testing.expectEqual(@as(u32, 0o750), cfg.dir_mode);
 69+    try std.testing.expectEqual(@as(u32, 0o640), cfg.log_mode);
 70+}
 71+
 72+test "Cfg.init uses custom modes from env vars" {
 73+    const alloc = std.testing.allocator;
 74+
 75+    // Set custom octal values
 76+    _ = cross.c.setenv("ZMX_DIR_MODE", "770", 1);
 77+    _ = cross.c.setenv("ZMX_LOG_MODE", "660", 1);
 78+    defer {
 79+        _ = cross.c.unsetenv("ZMX_DIR_MODE");
 80+        _ = cross.c.unsetenv("ZMX_LOG_MODE");
 81+    }
 82+
 83+    var cfg = try Cfg.init(alloc);
 84+    defer cfg.deinit(alloc);
 85+
 86+    try std.testing.expectEqual(@as(u32, 0o770), cfg.dir_mode);
 87+    try std.testing.expectEqual(@as(u32, 0o660), cfg.log_mode);
 88+}
 89+
 90+
 91 /// Daemon is responsible for managing a zmx session.
 92 ///
 93 /// It holds all the state for a running session.  Instead of a single daemon for all sessions, we
 94@@ -535,7 +582,7 @@ const Daemon = struct {
 95                     &.{ self.cfg.log_dir, session_log_name },
 96                 );
 97                 defer self.alloc.free(session_log_path);
 98-                try log_system.init(self.alloc, session_log_path);
 99+                try log_system.init(self.alloc, session_log_path, self.cfg.log_mode);
100 
101                 // If spawnPty fails, clean up here. Once it succeeds,
102                 // the inner block's defer takes ownership of cleanup to
103@@ -891,6 +938,8 @@ fn help() !void {
104         \\  - TMPDIR               Controls which folder is used to store unix socket files (prio: 3)
105         \\  - ZMX_SESSION          The session name we inject into every zmx session automatically
106         \\  - ZMX_SESSION_PREFIX   Adds this value to the start of every session name for all commands
107+        \\  - ZMX_DIR_MODE         Sets the mode for the socket and log directories (octal, defaults to 0750)
108+        \\  - ZMX_LOG_MODE         Sets the mode for the log files (octal, defaults to 0640)
109         \\
110     ;
111     var buf: [4096]u8 = undefined;