@mrsekut/ccron 0.3.0 → 0.3.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrsekut/ccron",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Schedule claude -p execution on macOS with launchd",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -25,7 +25,7 @@ Checks:
25
25
  2. claude auth status (logged in)
26
26
  3. ulimit -n (file descriptors)
27
27
  4. Script file exists
28
- 5. Script path is outside TCC-protected directories
28
+ 5. Script path (and cwd, if set) is outside TCC-protected directories
29
29
  6. Plist registered in launchd
30
30
  7. MCP config exists (if task uses MCP)
31
31
  8. MCP auth (if task uses MCP) — runs claude CLI to verify OAuth tokens
@@ -58,6 +58,7 @@ Example:
58
58
  checks.push(await checkUlimit());
59
59
  checks.push(await checkScriptExists(name));
60
60
  checks.push(checkScriptTccSafe(name));
61
+ if (task.cwd) checks.push(checkCwdTccSafe(task.cwd));
61
62
  checks.push(await checkPlistRegistered(name));
62
63
 
63
64
  if (task.mcpConfig) {
@@ -175,11 +176,32 @@ async function checkScriptExists(name: string): Promise<CheckResult> {
175
176
  };
176
177
  }
177
178
 
179
+ const TCC_DIRS = ['Desktop', 'Documents', 'Downloads'];
180
+
181
+ function tccProtects(path: string): boolean {
182
+ const home = homedir();
183
+ return TCC_DIRS.some(dir => path.startsWith(`${home}/${dir}`));
184
+ }
185
+
186
+ function checkCwdTccSafe(cwd: string): CheckResult {
187
+ const inTcc = tccProtects(cwd);
188
+ return {
189
+ label: 'TCC protection (cwd)',
190
+ ok: !inTcc,
191
+ detail: inTcc
192
+ ? `cwd is in a TCC-protected directory (${cwd})`
193
+ : `cwd is outside TCC-protected directories`,
194
+ // Granting Full Disk Access to /bin/bash is not enough: the claude process
195
+ // gets its own TCC identity and still fails with EPERM on these paths.
196
+ fix: inTcc
197
+ ? 'Move the project outside ~/Desktop, ~/Documents, ~/Downloads, or grant Full Disk Access to the claude binary'
198
+ : undefined,
199
+ };
200
+ }
201
+
178
202
  function checkScriptTccSafe(name: string): CheckResult {
179
203
  const path = scriptPath(name);
180
- const home = homedir();
181
- const tccDirs = [`${home}/Desktop`, `${home}/Documents`, `${home}/Downloads`];
182
- const inTcc = tccDirs.some(dir => path.startsWith(dir));
204
+ const inTcc = tccProtects(path);
183
205
  return {
184
206
  label: 'TCC protection',
185
207
  ok: !inTcc,
package/src/generator.ts CHANGED
@@ -59,6 +59,11 @@ export function generateScriptContent(
59
59
 
60
60
  /**
61
61
  * Generate the launchd plist XML content.
62
+ *
63
+ * WorkingDirectory stays /tmp even when the task sets `cwd`. launchd performs the
64
+ * chdir itself, and that chdir is denied for TCC-protected locations (~/Desktop,
65
+ * ~/Documents, ...), which fails the job before the script runs. The script's own
66
+ * `cd` handles `cwd` instead.
62
67
  */
63
68
  export function generatePlistContent(
64
69
  task: TaskConfig,
@@ -102,7 +107,7 @@ export function generatePlistContent(
102
107
  <string>${escapeXml(pathEntries)}</string>
103
108
  </dict>
104
109
  <key>WorkingDirectory</key>
105
- <string>${escapeXml(task.cwd ?? '/tmp')}</string>
110
+ <string>/tmp</string>
106
111
  <key>StartCalendarInterval</key>
107
112
  ${calendarIntervalXml}
108
113
  <key>StandardOutPath</key>