Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/init-global-agent-trust.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@taskless/cli": patch
---

Claude Code's auto mode can refuse a Taskless command even when the project allows that exact version, because its classifier overrules project allow rules for downloaded code and reads its own exceptions only from your user settings. The setup wizard (`npx @taskless/cli` with no arguments) now lists every settings edit together, as aligned `ALLOW (project):`, `REMOVE (project):`, and `ALLOW (global):` statements, each naming the command and the file, under one question, "Make these changes to your settings?", and on yes adds one entry to the top-level `autoMode.allow` list in `~/.claude/settings.json` (or under `CLAUDE_CONFIG_DIR`). The entry starts `Taskless CLI:`, names no version, and covers a Taskless call only when it names an exact version; it is added once, after `"$defaults"` when `init` creates the list, a later install that finds it leaves it alone, and one that does not offers it again. Only the interactive wizard writes it: the batch `init` never writes outside the project, and instead warns when the entry is missing, naming the file, the key, and the text to add. `init --json` reports it under `globalTrust`. When a classifier does refuse a Taskless command, the skill now tells the agent to stop without retrying and hand you that entry, rather than offering a project allow rule that cannot help.
2 changes: 1 addition & 1 deletion .changeset/pinned-cli-permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ Released builds now pin their own version. The installed skill and the `agent` r

`init` now also reports stale Taskless pins in CI and git-hook files (GitHub Actions, GitLab CI, CircleCI, husky, lefthook, pre-commit, and the rest of the files `detect` recognizes) by file and line, through the same notice and `pinnedCli` JSON field as `package.json` pins. It recommends the version to move to and never edits the file.

`init` now also allows exactly the installed CLI in each agent it installs for, so a harness that gates download-and-run commands lets Taskless through without a prompt: Claude Code (`.claude/settings.json`), Cursor (`.cursor/permissions.json` and `.cursor/cli.json`), OpenCode (`opencode.json[c]`), and Codex (`.codex/rules/taskless.rules`, when Codex is present). Each rule names one version and cannot match another. Every edit is announced, as `Edited <file>` with a green `ALLOW:` line, and the wizard shows it and asks before writing. An upgrade replaces only the rule Taskless recorded, a rule you wrote is never touched, and a file that denies the command or does not parse is left alone and reported. `init --json` carries the same edits under `permissions`. Agents Taskless does not install into use per-command approval through the skill.
`init` now also allows exactly the installed CLI in each agent it installs for, so a harness that gates download-and-run commands lets Taskless through without a prompt: Claude Code (`.claude/settings.json`), Cursor (`.cursor/permissions.json` and `.cursor/cli.json`), OpenCode (`opencode.json[c]`), and Codex (`.codex/rules/taskless.rules`, when Codex is present). Each rule names one version and cannot match another. Every edit is announced, as `Edited <file>` with a green `ALLOW:` line, and the wizard shows it and asks before writing. Each file ends up with exactly one Taskless rule, the installed version's: any other allow rule for `@taskless/cli` or `@taskless/cli-nightly`, whatever version it names and however it got there, is removed and shown as a red `REMOVE:` line. Deny rules and rules for anything else are never touched, and a file that denies the command or does not parse is left alone and reported. `init --json` carries the same edits under `permissions`. Agents Taskless does not install into use per-command approval through the skill.

The setup wizard's tool list now has one `.agents/` row, labelled with the detected harnesses that read it, such as `OpenCode, Codex, and other .agents harnesses (.agents/)`, instead of separate `Codex` and `Agent Skills` rows. Gemini CLI and Crush are now detected, and install the skill into `.agents/`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-15
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
## Context

`install/permissions.ts` writes one rule set per harness into project files, driven by `syncPermissions`, and decides what to remove from the manifest's `install.permissions` record: it removes exactly what it recorded and nothing else. The wizard calls it twice, once with `write: false` to preview in the summary step and once to write; `--no-interactive` calls it once and prints the edits afterwards. The skill body source is `skills/taskless/SKILL.md`, 121 lines against a 150-line cap enforced in `installed-documentation.test.ts`.

Claude Code's auto mode reads `autoMode.{environment,allow,soft_deny,hard_deny}` as prose lists from user settings, where `"$defaults"` inside a list splices in the shipped entries. `claude auto-mode critique` reported "No custom auto mode rules found" when the section lived in a project's `.claude/settings.json`, which confirms the project file is not read for it.

## Goals / Non-Goals

**Goals:**

- One Taskless rule per project permission file, regardless of how earlier rules got there.
- A shape for user-level trust that a second harness can join by adding one entry, without touching `syncPermissions` or the wizard.
- No path by which a non-interactive or agent-driven `init` writes outside the project.

**Non-Goals:**

- Codex. Its guardian auto-review appears to review only commands that would otherwise prompt, so a matching `prefix_rule` allow, which `init` already writes, should pre-empt it. That claim comes from third-party write-ups that disagree on config key names; it is not verified, so Codex does not join the table until it is.
- Removing the global entry on uninstall. It names no repo and covers every Taskless project the user has, so one repo's uninstall is not a reason to remove it.
- Proving the entry clears the classifier. That is a judgement by a model and cannot be tested in CI; see Risks.

## Decisions

**Recognize Taskless rules by pattern per harness, not by the record.** Each `Harness` gains `found(text): string[]`, returning every Taskless allow rule in its file, written the way its `edit` takes them in `remove`, and matching its own format: Claude `Bash(<cmd> *)` and `Bash(<cmd>:*)`; Cursor editor `<cmd>`; Cursor CLI `Shell(npx:<spec>)` and `Shell(npx:<spec> *)`; OpenCode object keys and array resources; Codex `prefix_rule` patterns. `<cmd>` is `npx` or `pnpm dlx` followed by `@taskless/cli` or `@taskless/cli-nightly`, with or without an `@<anything>` suffix. Each harness's `edit` already receives a `remove` list, so `syncPermissions` reads every file first and computes `remove` as `found` minus the wanted rules, instead of the recorded rules minus the wanted ones. The record still supplies `created`, for the delete-when-empty rule. _Alternative:_ keep the record as the authority and add a one-time migration that adopts existing Taskless rules into it. Rejected: it only fixes rules present at migration time, and the skill's fallback can add a new unrecorded rule at any point.

Deny-shaped entries are never found, because `found` reads only allow lists (Claude `permissions.allow`, Cursor `terminalAllowlist` and `permissions.allow`, OpenCode `"allow"` values and `effect: "allow"`, Codex `decision="allow"`).

**A separate `GlobalTrust` interface, not another `Harness`.** It has a different lifecycle: it is not recorded, not versioned, is written at most once, and is never removed. Folding it into `Harness` would put conditionals for all four through `syncPermissions`. Shape:

```ts
interface GlobalTrust {
harness: string; // "Claude Code"
selectedBy: string; // ".claude"
path(environment: { home; env }): string; // honours CLAUDE_CONFIG_DIR
key: readonly string[]; // ["autoMode", "allow"]
seed: readonly string[]; // ["$defaults", entry], for a new list
}
```

Detection and the edit are shared, not per harness: an entry is present when the list holds a string starting with the label, and the edit appends the entry, or writes `seed` when the list is absent. `syncGlobalTrust({ selected, write, environment })` returns one outcome per applicable entry: `present`, `absent`, `written`, or `skipped` with a reason. `environment` defaults to the real home directory and `process.env`; tests pass their own, and the spawned-CLI tests set `HOME`, so no test reaches the real home directory. `GLOBAL_TRUST` is a one-element list; joining it is the only change a second harness needs.

**The entry text**, fixed in source and identical across versions:

> Taskless CLI: The npm packages @taskless/cli and @taskless/cli-nightly are tooling published by Taskless via npm trusted publishing (OIDC), installed by the user through the Taskless installer. Running either via npx or pnpm dlx is allowed only when the call names an exact version (e.g. @taskless/cli@1.2.3). Dist-tags such as @latest or @nightly, version ranges, and unversioned calls are not covered.

It follows the shipped entries' `Name: description` form, which is also what makes label detection possible. The example version is deliberately fake so the text never ages.

**`$defaults` only when creating the list.** An existing list without it is a choice the user made, to replace the defaults; adding it back would widen what their classifier allows. Edits go through the same `jsonc-parser` edit path as the project files, so formatting and comments survive.

**One confirmation in the wizard, covering every settings edit.** The summary groups the project and global edits under one heading, as aligned `ALLOW (project):`, `REMOVE (project):`, and `ALLOW (global):` statements, and asks `Make these changes to your settings?`, defaulting to yes. Declining cancels the install and writes nothing, the same meaning the project confirm already had. _Alternative:_ a separate global confirm after the project one, so a user could set up the repo without touching their home directory. Rejected for the friction: two questions for one install, and the second asked on every run until the entry exists, since a decline is never recorded.

**The warning is computed after writing.** Both modes call `syncGlobalTrust` read-only after the install and warn for each `absent` outcome, which covers the batch install, and a wizard install whose user file was skipped, with one code path.

## Risks / Trade-offs

- [The classifier ignores or misreads the entry] → The entry is guidance to a model, not a rule. Mitigation: the skill's fallback still handles a refusal, and it now gives the entry inline, so a user without it, or with a wording that does not land, still gets a path. The wording can change without a migration because detection is by label; existing users keep the old text, which is acceptable since it is still a valid entry.
- [Pattern removal deletes a Taskless rule the user wanted] → By decision, one line per file and it is the installed one. Every removal is shown as `REMOVE:` in the wizard preview before anything is written.
- [A recognizer is too broad and removes a non-Taskless rule] → The command must be `npx` or `pnpm dlx` immediately followed by one of two exact package names; tested per harness with near-misses (`@taskless/cli-foo`, `npx -y @taskless/cli`, other packages).
- [Writing to `~/.claude/settings.json` races a running Claude Code session] → Claude Code rewrites its settings file itself. Mitigation: read-modify-write in one step, immediately after the confirm, through a minimal `jsonc-parser` edit rather than a re-serialization.
- [The skill body grows] → About 15 lines against 29 lines of headroom under the cap.

## Migration Plan

None beyond the release. The first `init` after upgrading removes stale Taskless rules and shows them as `REMOVE:` lines. Rollback is a downgrade; stale rules removed by the new version are not restored, which is the intended state anyway.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
## Why

A user onboarding in Claude Code's auto mode was stopped at `npx @taskless/cli-nightly@<version> info --json`, which the classifier labelled "Code from External", even though `.claude/settings.json` allowed exactly that invocation. Auto mode does not trust a project allow rule that runs downloaded code, and it does not read the `autoMode` section from a checked-in project file at all, so nothing `init` writes into the repo can clear it. The user already made that trust decision when they ran `npx @taskless/cli init`; the classifier cannot see it, because it judges only the session. The only place that decision can be carried into the session is the user's own settings, and the only moment a human is present to make it is the interactive install.

The same investigation showed the project files drifting: an older Taskless rule for a previous nightly sat next to the current one, because it was added through the skill's fallback rather than by `init`, so the manifest never recorded it and an upgrade left it in place. A pinned rule for a version that is no longer installed allows nothing anyone wants.

## What Changes

- **Project permission files hold exactly one Taskless rule.** `init` removes every Taskless allow rule in a harness file it manages, whatever version it names and whether or not the manifest recorded it, and writes the rule for the installed invocation. Each removal is reported as a `REMOVE:` line. Deny rules and non-Taskless entries are never touched. **BREAKING** for the current spec: a Taskless rule the user wrote no longer survives an upgrade.
- **A per-harness global trust slot.** For a harness whose safety classifier can overrule a project allow rule, the interactive wizard offers to write one entry to that harness's user-level settings. Only Claude Code is implemented: a top-level `autoMode.allow` entry in `~/.claude/settings.json`, labelled `Taskless CLI:`, that names no version and states that a call is covered only when it names an exact version. When `init` creates the `allow` list it seeds it with `"$defaults"`, so the shipped exceptions stay in force.
- **Written once, confirmed separately, interactive only.** The entry is found by its label, so a later install sees it and leaves it alone. The wizard shows it in its summary as an edit to a file outside the project and asks for its own confirmation. `--no-interactive` never writes outside the project. Nothing about it is recorded in the repository manifest, which is shared by everyone who clones the repo.
- **A warning when it is missing.** Both modes read the user file and, when the entry is absent, print a warning naming the file and the entry to add.
- **The skill's blocked-command fallback is self-contained.** When a Taskless command is refused by an agent's safety classifier, the installed skill tells the agent to stop, not retry, and give the user the global entry, inline. It cannot point at an `agent <topic>` recipe, because the recipe is fetched through the same blocked `npx` call. For Claude Code's auto mode it stops offering a project allow rule, which the classifier overrules.

## Capabilities

### New Capabilities

- `agent-global-trust`: the user-level trust entry for harnesses whose classifier overrules project allow rules: which harnesses have one, its wording, how it is detected, when it is offered and written, and the warning when it is absent.

### Modified Capabilities

- `cli-init`: the manifest-recorded-rules requirement becomes "exactly one Taskless rule per permission file", removing unrecorded Taskless rules; the permission-edit report gains the user-level file.
- `skill-taskless`: the blocked-preflight fallback distinguishes a classifier refusal, carries the global entry inline, and drops the project allow-rule offer where the classifier overrules it.

## Impact

- `packages/cli/src/install/permissions.ts`: per-harness recognition of a Taskless rule in each file format (including the `Bash(<invocation>:*)` form the skill used to propose); removal of unrecorded Taskless rules; the global trust slot and its Claude Code implementation.
- `packages/cli/src/wizard/index.ts`, `wizard/steps/summary.ts`: preview and separate confirm for the user-level edit.
- `packages/cli/src/commands/init.ts`: the missing-entry warning in `--no-interactive`; the `--json` envelope gains the global trust outcome.
- `packages/cli/src/agent/init.md` (the `init --json` shape) and the installed skill body template.
- Tests in `packages/cli/test/permissions.test.ts` and the wizard and skill-body tests. No home-directory writes in tests: the user settings path is injected.
- Delivery shape: **single PR**. The pieces are only meaningful together (the warning, the wizard offer, and the skill fallback all point at the same entry), and the whole change fits one reviewable diff.
Loading
Loading