e1ccc4573d0f

home apply/check: thread ctx.passthrough into home-manager switch/build argv, home-manager build's shell wrapper takes extra flags via "$@" to avoid injection; bump v3.4.12

time
2026-07-02 00:58 UTC · 3 months ago
machine
koh2
agent
claude-sonnet-5
session
nina passthrough flag audit
parent
c93535d54e46
4 files VERSION+1 −1src/commands.zig+4 −4src/exec.zig+21 −6src/main.zig+12 −0
VERSION+1 −1
@@ -1 +1 @@1-3.4.111+3.4.12

view file

src/commands.zig+4 −4
@@ -1474,7 +1474,7 @@14741474 14751475 fn homeCheck(ctx: Ctx) !void {14761476     output.homeCheckStart(ctx.machine.name);1477-    const code = try exec.homeManagerBuild(ctx.io, &ctx.machine, ctx.gpa);1477+    const code = try exec.homeManagerBuild(ctx.io, &ctx.machine, ctx.gpa, ctx.passthrough);14781478     if (code != 0) return error.BuildFailed;14791479     output.homeCheckOk();14801480 }@@ -1559,7 +1559,7 @@15591559     }15601560     if (ctx.dry) {15611561         output.homeApplyDry(ctx.machine.name);1562-        const code = try exec.homeManagerBuild(ctx.io, &ctx.machine, ctx.gpa);1562+        const code = try exec.homeManagerBuild(ctx.io, &ctx.machine, ctx.gpa, ctx.passthrough);15631563         if (code != 0) return error.BuildFailed;15641564         output.homeApplyDryDone();15651565         return;@@ -1580,7 +1580,7 @@15801580 15811581     output.printSubstep("home-manager switch", .{});15821582     const start = std.Io.Clock.awake.now(ctx.io);1583-    const code = try exec.homeManagerSwitch(ctx.io, &ctx.machine, ctx.gpa);1583+    const code = try exec.homeManagerSwitch(ctx.io, &ctx.machine, ctx.gpa, ctx.passthrough);15841584     if (code != 0) return error.BuildFailed;15851585 15861586     const elapsed_ns = @max(0, start.untilNow(ctx.io, .awake).toNanoseconds());@@ -1611,7 +1611,7 @@16111611     const start = std.Io.Clock.awake.now(ctx.io);16121612     for (refs.items, 0..) |ref, i| {16131613         output.printSubstep("home-manager switch --flake {s}", .{ref});1614-        switch (try exec.homeManagerSwitchFlake(ctx.io, &ctx.machine, ctx.gpa, ref)) {1614+        switch (try exec.homeManagerSwitchFlake(ctx.io, &ctx.machine, ctx.gpa, ref, ctx.passthrough)) {16151615             .ok => {16161616                 const elapsed_ns = @max(0, start.untilNow(ctx.io, .awake).toNanoseconds());16171617                 const elapsed: u64 = @intCast(@divFloor(elapsed_ns, 1_000_000));

view file

src/exec.zig+21 −6
@@ -1666,9 +1666,13 @@16661666     return stream(io, machine, gpa, argv.items);16671667 }16681668 1669-pub fn homeManagerSwitch(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator) !u8 {1669+pub fn homeManagerSwitch(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator, extra_flags: []const []const u8) !u8 {16701670     output.flush();1671-    const r = try capture(gpa, io, machine, &.{ "home-manager", "switch" });1671+    var argv: std.ArrayList([]const u8) = .empty;1672+    defer argv.deinit(gpa);1673+    try argv.appendSlice(gpa, &.{ "home-manager", "switch" });1674+    try argv.appendSlice(gpa, extra_flags);1675+    const r = try capture(gpa, io, machine, argv.items);16721676     defer gpa.free(r.stdout);16731677     defer gpa.free(r.stderr);16741678     errors.setBuildStderr(r.stderr);@@ -1680,9 +1684,13 @@16801684 // genuine build failure.16811685 pub const SwitchOutcome = enum { ok, attr_missing, failed };16821686 1683-pub fn homeManagerSwitchFlake(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator, flake_ref: []const u8) !SwitchOutcome {1687+pub fn homeManagerSwitchFlake(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator, flake_ref: []const u8, extra_flags: []const []const u8) !SwitchOutcome {16841688     output.flush();1685-    const r = try capture(gpa, io, machine, &.{ "home-manager", "switch", "--flake", flake_ref });1689+    var argv: std.ArrayList([]const u8) = .empty;1690+    defer argv.deinit(gpa);1691+    try argv.appendSlice(gpa, &.{ "home-manager", "switch", "--flake", flake_ref });1692+    try argv.appendSlice(gpa, extra_flags);1693+    const r = try capture(gpa, io, machine, argv.items);16861694     defer gpa.free(r.stdout);16871695     defer gpa.free(r.stderr);16881696     if (r.exit_code == 0) return .ok;@@ -1710,9 +1718,16 @@17101718 // check` and `home apply --dry`. `home-manager build` drops a ./result symlink17111719 // in the cwd, so run it in a throwaway temp dir to avoid littering the directory17121720 // nina was invoked from.1713-pub fn homeManagerBuild(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator) !u8 {1721+pub fn homeManagerBuild(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator, extra_flags: []const []const u8) !u8 {17141722     output.flush();1715-    const r = try capture(gpa, io, machine, &.{ "sh", "-c", "d=$(mktemp -d) && cd \"$d\" && home-manager build" });1723+    // extra_flags are passed as real argv elements after the literal `--`, picked1724+    // up via "$@" — never interpolated into the script text, so an extra flag1725+    // can't smuggle shell metacharacters into the command.1726+    var argv: std.ArrayList([]const u8) = .empty;1727+    defer argv.deinit(gpa);1728+    try argv.appendSlice(gpa, &.{ "sh", "-c", "d=$(mktemp -d) && cd \"$d\" && home-manager build \"$@\"", "--" });1729+    try argv.appendSlice(gpa, extra_flags);1730+    const r = try capture(gpa, io, machine, argv.items);17161731     defer gpa.free(r.stdout);17171732     defer gpa.free(r.stderr);17181733     errors.setBuildStderr(r.stderr);

view file

src/main.zig+12 −0
@@ -445,6 +445,18 @@445445     try std.testing.expectEqualStrings("--version", args.passthrough[1]);446446 }447447 448+test "parse args forwards home apply --show-trace" {449+    const gpa = std.testing.allocator;450+    const argv = [_][:0]const u8{ "nina", "home", "apply", "--show-trace" };451+    const args = try parseArgs(gpa, &argv);452+    defer gpa.free(args.rest);453+    defer gpa.free(args.passthrough);454+    try std.testing.expectEqualStrings("home", args.command);455+    try std.testing.expectEqualStrings("apply", args.sub);456+    try std.testing.expectEqual(@as(usize, 1), args.passthrough.len);457+    try std.testing.expectEqualStrings("--show-trace", args.passthrough[0]);458+}459+448460 test "parse subcommand" {449461     const gpa = std.testing.allocator;450462     const argv = [_][:0]const u8{ "nina", "service", "list" };

view file