agy-powerline 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.0.2] - 2026-10-11
9
+
10
+ ### Security
11
+
12
+ - On macOS and Linux, the config, `exec` cache, problem log and payload dump are now readable by your account only, and saving Antigravity's `settings.json` keeps its permissions. Run `agy-powerline init --yes` after updating to apply this to existing files (#11)
13
+
8
14
  ## [1.0.1] - 2026-10-11
9
15
 
10
16
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agy-powerline",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Interactive statusline for Google Antigravity",
5
5
  "bin": {
6
6
  "agy-powerline": "src/index.js"
package/src/cli/init.js CHANGED
@@ -11,6 +11,7 @@ const {
11
11
  readSettingsForUpdate,
12
12
  settingsBackupPath,
13
13
  writeJsonSafely,
14
+ restrictPrivateFiles,
14
15
  isWindowsNodeOnWsl,
15
16
  windowsNodeOnWslMessage,
16
17
  cmdBreakingChars,
@@ -81,6 +82,7 @@ async function initCommand(options = {}) {
81
82
 
82
83
  try {
83
84
  fs.mkdirSync(installDir, { recursive: true });
85
+ restrictPrivateFiles(installDir);
84
86
 
85
87
  const destStatusline = path.join(installDir, "statusline.js");
86
88
  const destConfig = path.join(installDir, "statusline.config.json");
package/src/lib/config.js CHANGED
@@ -6,7 +6,7 @@ const { resolveColorModeId } = require("./colors");
6
6
  const { resolveThemeId } = require("./themes");
7
7
  const { stripControl, normalizeClock } = require("./formatters");
8
8
  const { normalizePowerline } = require("./powerline");
9
- const { writeFileAtomic } = require("./paths");
9
+ const { writeFileAtomic, PRIVATE_FILE_MODE } = require("./paths");
10
10
 
11
11
  // Display text in widget configs is printed as-is, so strip terminal control
12
12
  // from it. cmd/command are left alone: they are executed, not printed.
@@ -225,7 +225,9 @@ function saveConfig(config) {
225
225
  throw new InvalidConfigError(getConfigPath(), current.error);
226
226
  }
227
227
 
228
- writeFileAtomic(getConfigPath(), JSON.stringify(mergeConfig(config), null, 2) + "\n");
228
+ writeFileAtomic(getConfigPath(), JSON.stringify(mergeConfig(config), null, 2) + "\n", {
229
+ mode: PRIVATE_FILE_MODE,
230
+ });
229
231
  }
230
232
 
231
233
  // Presets set the widget list only; the layout (Pills or Lines) stays the one
package/src/lib/exec.js CHANGED
@@ -7,6 +7,7 @@ const crypto = require("crypto");
7
7
  const { execSync } = require("child_process");
8
8
  const { stripControl } = require("./formatters");
9
9
  const { logWarning } = require("./log");
10
+ const { PRIVATE_FILE_MODE } = require("./paths");
10
11
 
11
12
  const DEFAULT_TIMEOUT_MS = 500;
12
13
  const DEFAULT_INTERVAL_SEC = 10;
@@ -54,7 +55,8 @@ function loadDiskCache() {
54
55
 
55
56
  // Several agy windows refresh at once, so the cache is written to a fresh temp
56
57
  // file and renamed into place: a reader never sees half a file. "wx" refuses a
57
- // file that already exists (or a planted link) in a shared temp folder.
58
+ // file that already exists (or a planted link) in a shared temp folder, and the
59
+ // file is owner-only because it holds the output of the user's commands.
58
60
  function saveDiskCache(cache) {
59
61
  let tmp = "";
60
62
  try {
@@ -64,7 +66,11 @@ function saveDiskCache(cache) {
64
66
  fs.mkdirSync(dir, { recursive: true });
65
67
  }
66
68
  tmp = `${file}.${process.pid}.${crypto.randomBytes(4).toString("hex")}.tmp`;
67
- fs.writeFileSync(tmp, JSON.stringify(cache), { encoding: "utf8", flag: "wx" });
69
+ fs.writeFileSync(tmp, JSON.stringify(cache), {
70
+ encoding: "utf8",
71
+ flag: "wx",
72
+ mode: PRIVATE_FILE_MODE,
73
+ });
68
74
  fs.renameSync(tmp, file);
69
75
  } catch {
70
76
  // The cache is best effort: skip this write rather than risk a torn file.
package/src/lib/log.js CHANGED
@@ -9,7 +9,7 @@
9
9
  const fs = require("fs");
10
10
  const os = require("os");
11
11
  const path = require("path");
12
- const { writeFileAtomic, readInstallMeta } = require("./paths");
12
+ const { writeFileAtomic, readInstallMeta, PRIVATE_FILE_MODE } = require("./paths");
13
13
  const { stripControl } = require("./formatters");
14
14
 
15
15
  const LOG_NAME = "agy-powerline.log";
@@ -246,7 +246,7 @@ function logEvent(level, code, details = {}) {
246
246
  lines = lines.slice(1);
247
247
  }
248
248
 
249
- writeFileAtomic(file, lines.join("\n") + "\n");
249
+ writeFileAtomic(file, lines.join("\n") + "\n", { mode: PRIVATE_FILE_MODE });
250
250
  } catch {
251
251
  // Read-only disk, full disk, permissions: the bar matters more.
252
252
  }
package/src/lib/paths.js CHANGED
@@ -28,17 +28,25 @@ const RUNTIME_FILES = [
28
28
  path.join("layouts", "pills.js"),
29
29
  ];
30
30
 
31
+ // Files that can hold the user's commands, their output or session details.
32
+ // They are written readable by the owner only, so other accounts on a shared
33
+ // machine can't read them (Windows profile folders are private already).
34
+ const PRIVATE_FILES = [
35
+ "statusline.config.json",
36
+ ".exec-cache.json",
37
+ "last-payload.json",
38
+ // LOG_NAME in log.js, spelled out because log.js requires this file.
39
+ "agy-powerline.log",
40
+ ];
41
+ const PRIVATE_FILE_MODE = 0o600;
42
+
31
43
  // Everything init or the runtime may create in the install dir. uninstall
32
44
  // --delete removes only these, never the whole directory tree.
33
45
  const INSTALLED_FILES = [
34
46
  "statusline.js",
35
47
  "statusline.cmd",
36
48
  INSTALL_META_NAME,
37
- "statusline.config.json",
38
- ".exec-cache.json",
39
- "last-payload.json",
40
- // LOG_NAME in log.js, spelled out because log.js requires this file.
41
- "agy-powerline.log",
49
+ ...PRIVATE_FILES,
42
50
  ...RUNTIME_FILES,
43
51
  ];
44
52
 
@@ -238,11 +246,24 @@ function writeJsonSafely(file, data) {
238
246
  }
239
247
 
240
248
  // Writes via temp file + rename so an interrupted write cannot leave a
241
- // truncated file behind.
242
- function writeFileAtomic(file, body) {
249
+ // truncated file behind. The new file gets `mode` when given, otherwise the
250
+ // mode the existing file had, so rewriting a file never widens who can read
251
+ // it (the rename would otherwise reset it to the default).
252
+ function writeFileAtomic(file, body, { mode } = {}) {
243
253
  fs.mkdirSync(path.dirname(file), { recursive: true });
244
254
  const tmp = `${file}.${process.pid}.tmp`;
245
- fs.writeFileSync(tmp, body, "utf8");
255
+ const target = process.platform === "win32" ? undefined : (mode ?? existingMode(file));
256
+ fs.writeFileSync(tmp, body, { encoding: "utf8", mode: target ?? 0o666 });
257
+ if (target !== undefined) {
258
+ // The mode passed to writeFileSync is reduced by the umask; set it exactly.
259
+ // Some filesystems (e.g. Windows drives under WSL) refuse chmod; the write
260
+ // still matters more.
261
+ try {
262
+ fs.chmodSync(tmp, target);
263
+ } catch {
264
+ // Keep the mode writeFileSync gave it.
265
+ }
266
+ }
246
267
 
247
268
  try {
248
269
  fs.renameSync(tmp, file);
@@ -253,6 +274,29 @@ function writeFileAtomic(file, body) {
253
274
  }
254
275
  }
255
276
 
277
+ function existingMode(file) {
278
+ try {
279
+ return fs.statSync(file).mode & 0o777;
280
+ } catch {
281
+ return undefined;
282
+ }
283
+ }
284
+
285
+ // Installs made before the private-file change left these files readable by
286
+ // everyone; init narrows them. Best effort: a file we can't chmod stays as is.
287
+ function restrictPrivateFiles(installDir) {
288
+ if (process.platform === "win32") {
289
+ return;
290
+ }
291
+ for (const name of PRIVATE_FILES) {
292
+ try {
293
+ fs.chmodSync(path.join(installDir, name), PRIVATE_FILE_MODE);
294
+ } catch {
295
+ // Missing file, or not ours to change.
296
+ }
297
+ }
298
+ }
299
+
256
300
  function removeSettingsFile(file = AGY_SETTINGS) {
257
301
  if (fs.existsSync(file)) {
258
302
  fs.copyFileSync(file, settingsBackupPath(file));
@@ -305,6 +349,9 @@ module.exports = {
305
349
  INSTALL_META_NAME,
306
350
  RUNTIME_FILES,
307
351
  INSTALLED_FILES,
352
+ PRIVATE_FILES,
353
+ PRIVATE_FILE_MODE,
354
+ restrictPrivateFiles,
308
355
  resolveInstallDir,
309
356
  isTrustedInstallDir,
310
357
  statuslineCommand,
@@ -7,9 +7,9 @@
7
7
  // branch names become placeholders. A field Antigravity adds later is left
8
8
  // out until it is listed here.
9
9
 
10
- const fs = require("fs");
11
10
  const path = require("path");
12
11
  const { stripControl } = require("./formatters");
12
+ const { writeFileAtomic, PRIVATE_FILE_MODE } = require("./paths");
13
13
 
14
14
  const NUMBER = "number";
15
15
  const BOOL = "boolean";
@@ -160,9 +160,9 @@ function writePayloadDump(target, rawInput) {
160
160
  }
161
161
 
162
162
  try {
163
- const resolved = path.resolve(target);
164
- fs.mkdirSync(path.dirname(resolved), { recursive: true });
165
- fs.writeFileSync(resolved, JSON.stringify(body, null, 2) + "\n", "utf8");
163
+ writeFileAtomic(path.resolve(target), JSON.stringify(body, null, 2) + "\n", {
164
+ mode: PRIVATE_FILE_MODE,
165
+ });
166
166
  } catch {
167
167
  // Best-effort debug write; the bar matters more.
168
168
  }