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
16 changes: 16 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,22 @@

### Added

- **Machines can follow a signed managed state (`fastskill managed`).** A managed state is a
signed, resolved list of the skills one user's agents should have, decided somewhere else. The
source and the Ed25519 verification keys come only from trusted settings (the administrator's
system file or the user's `managed.toml`), never from a project. `managed enroll` and
`managed apply` verify the state, deploy the listed skills into each agent's user skills folder,
remove the ones FastSkill placed that are no longer listed, and move blocked content into a
quarantine instead of deleting it. While a user follows a state, every command that adds or
replaces skill content is checked by digest first; `skill list` reports `managed-blocked` and
`managed-not-allowed`, and `--check` fails on them. An `https://` source can name a credential
command for sign-in, receive a fixed, inspectable report after each apply, honor `editable` and
point refusals at a `request_url`. Session-start hooks in Claude Code and Gemini CLI and a
per-user timer (at login, then hourly) keep machines up to date, and `managed hooks --system`
prints what an administrator installs machine-wide. Nothing changes until a source is
configured. See [ADR-0016](docs/adr/0016-machines-follow-a-signed-managed-state.md), the
[managed state](webdocs/skill-management/managed-state.mdx) format and
[Hardening managed machines](webdocs/security/managed-machines.mdx).
- **`storage::git_isolated::clone_isolated` for cloning repositories you do not control.** The
library clone reads no system, global or user git configuration and inherits no environment
beyond `PATH`, proxy and CA settings, so a host's credential helper, `insteadOf` rewrite or
Expand Down
42 changes: 39 additions & 3 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,39 @@ A preset skill selection that a user may replace within the preset's customizati
**Permitted addition**:
A skill outside the preset's selections that its customization rules allow a user to add.

### Managed machines

**Managed state**:
A signed, resolved list of the skills one user's agents on one machine should have, decided
somewhere else and never by FastSkill. It travels in a DSSE envelope signed with Ed25519, is
verified against keys pinned in trusted settings, is bound to its **managed source** and its
`subject`, never goes backwards and expires. Resolved content like a Lock, not intent like a
Manifest. See [ADR-0016](./docs/adr/0016-machines-follow-a-signed-managed-state.md).
_Avoid_: policy, profile (the state is what a machine follows, not why).

**Managed source**:
The one location a machine fetches its managed state from: an `https://` URL or a local file path,
set only in trusted settings (the administrator's system file or the user's `managed.toml`), never
by a project. `report_url`, `request_url` and `editable` take effect only with an `https://`
source.

**Managed store**:
FastSkill's private, per-user copy of the skills a managed state lists, from which `managed apply`
deploys into each **Agent target** through a staging area. The cached state, the enrollment and
ownership records and the quarantine live next to it.

**Agent target**:
One agent's per-user skills folder, as `aikit-sdk` describes it, detected on every apply.
`managed apply` deploys to the fewest folders each agent reads and changes only the entries it
placed. The current project's skills folder is never an Agent target.

**Quarantine**:
Where `managed apply` moves content aside instead of deleting it: content whose digest is blocked,
content the state doesn't allow when it claims the Agent targets exclusively, and a deployed copy
that was changed after FastSkill placed it. Each item keeps its former location, its digest and
the reason; `managed unenroll` keeps the quarantine.
_Avoid_: delete, remove (quarantined content is kept).

### Discovery axes

These two axes — not the verb names — are what actually distinguish the read-side commands. The verbs should expose them, not hide them.
Expand Down Expand Up @@ -291,9 +324,10 @@ A proposed selection of behaviors and boundaries to evaluate for a skill, before

## Resolved decisions

- **Historical `sync` is removed.** Modern targets read skills directly. Propagation has exactly two
members: `project install` (Manifest → skills directory) and `index rebuild` (skills directory →
Vector index, conditional on an Embedding provider).
- **Historical `sync` is removed.** Modern targets read skills directly. Propagation has exactly
three members: `project install` (Manifest → skills directory), `index rebuild` (skills directory
→ Vector index, conditional on an Embedding provider) and `managed apply` (managed state →
managed store → Agent targets).
- **`index rebuild` is conditional.** It runs only when an **Embedding provider** is configured.
Mutating commands may auto-run it only when embeddings are enabled; `--reindex` and
`--no-reindex` override configuration. `cli doctor` surfaces provider state.
Expand All @@ -308,3 +342,5 @@ A proposed selection of behaviors and boundaries to evaluate for a skill, before
and explicit latest-stable resolution.
- [ADR-0010](./docs/adr/0010-command-taxonomy.md) defines the command namespaces and breaking
migration policy.
- [ADR-0016](./docs/adr/0016-machines-follow-a-signed-managed-state.md) defines the managed
state, how a machine follows it, and what FastSkill never decides.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ agents — there is no metadata-file sync step. FastSkill manages the files; you
- **Install skills** from a local folder, a git repo (branch/tag/subdirectory), a zip URL, or a registry ID.
- **Keep installs reproducible** with `skill-project.toml` + `skills.lock`; split optional vs production skills into groups.
- **Ship a complete team setup** as a verified, versioned bundle containing its full skill dependency closure.
- **Follow a managed state** on managed machines: `fastskill managed` verifies a signed skill list against pinned keys, deploys it to each agent, quarantines blocked content and gates installs by digest.
- **Discover skills** by meaning with semantic search (remote catalogs by default, `--local` for installed skills).
- **Test skill quality** with eval suites (`fastskill eval`) before you ship — each case runs isolated in a scratch workspace with only your skill, so trigger rates are reproducible across machines.
- **Improve skills automatically** with the text-gradient optimizer (`fastskill optimization`).
Expand Down Expand Up @@ -202,6 +203,7 @@ exports. It refuses a `tools/call` naming one with JSON-RPC
| `fastskill optimization` | `run`, `resume`, `status`, `inspect`, `export` | Optimize a skill document and inspect runs. |
| `fastskill index` | `rebuild` | Rebuild the local semantic index. |
| `fastskill cache` | `info`, `clean` | Inspect and reclaim cached content. |
| `fastskill managed` | `enroll`, `apply`, `status`, `unenroll`, `hooks` | Follow a signed managed state on this machine. |
| `fastskill server` | `serve` | Run the local HTTP API and web UI. |
| `fastskill mcp` | `serve`, `install`, `list` | Run and configure the MCP server. |
| `fastskill cli` | `doctor`, `completion`, `spec` | Diagnose FastSkill and export CLI metadata. |
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/0016-machines-follow-a-signed-managed-state.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Machines can follow a signed managed state; FastSkill applies it and never decides it

Status: accepted. Date: 2026-09-24. Revised: 2026-09-24. Amended: 2026-09-24. Accepted:
2026-10-09.
2026-10-09. Implemented: 2026-10-10.

Related: [ADR-0001](0001-remove-sync-command.md),
[ADR-0003](0003-serve-trust-boundary-and-edge-auth.md),
Expand Down
12 changes: 12 additions & 0 deletions webdocs/cheatsheet.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,18 @@ Modern agents (Claude Code, Cursor, …) read skills directly from the skills di

See [Registry overview](/registry/overview) and [`repo` commands](/cli-reference/repository-command).

## Managed machines

| Operation | Command | What it does |
|-----------|---------|--------------|
| Enroll | `fastskill managed enroll` | Follows the source in trusted settings, applies its first state, registers session-start hooks and the timer |
| Apply | `fastskill managed apply` | Fetches and applies the newest signed state; `--json` for the result |
| Status | `fastskill managed status` | Source, subject, expiry, sign-in, targets and quarantine; `--report` adds what the last apply did and the report body |
| Hooks | `fastskill managed hooks --system` | What an administrator installs machine-wide: the hook entry per agent and the timer files |
| Unenroll | `fastskill managed unenroll` | Removes what the state put here for this user; the quarantine stays |

See [`managed`](/cli-reference/managed-command), the [managed state](/skill-management/managed-state) format and [Hardening managed machines](/security/managed-machines).

## Local server

| Operation | Command | What it does |
Expand Down
4 changes: 4 additions & 0 deletions webdocs/cli-reference/managed-command.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ Only FastSkill's own entries are changed. A skill folder that FastSkill didn't p
holds other content than the state lists is reported as a collision and left alone.
</Callout>

The envelope and the payload are documented in [Managed state](/skill-management/managed-state).
What an administrator pins through device management, and what FastSkill can't enforce, is in
[Hardening managed machines](/security/managed-machines).

## Usage

```bash
Expand Down
4 changes: 4 additions & 0 deletions webdocs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ The project is designed around a **CLI-first workflow**; local **`fastskill serv
<Card title="Catalogs & sources">
`repo` for remote origins and catalog browsing. See [Registry overview](/registry/overview).
</Card>

<Card title="Managed machines">
`fastskill managed` follows a signed managed state: verified against pinned keys, deployed to each agent, blocked content quarantined, installs gated by digest. See [managed command](/cli-reference/managed-command).
</Card>
</Cards>

## Start with one local skill
Expand Down
169 changes: 169 additions & 0 deletions webdocs/security/managed-machines.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
---
title: "Hardening managed machines"
description: "What FastSkill enforces on a machine that follows a managed state, what it cannot, and what an administrator pins through device management: the system file, keys, hooks and the machine-wide timer."
---

# Hardening managed machines

[`fastskill managed`](/cli-reference/managed-command) is cooperative configuration management
with integrity checks. It is not a sandbox. This page says where FastSkill enforces a
[managed state](/skill-management/managed-state), where it can't, and what an administrator pins so
that following the state is the default path on every machine.

## What FastSkill enforces

FastSkill enforces the state at its own operations, and nowhere else:

- **`managed apply`** deploys the listed skills into each agent's user skills folder, removes the
ones it placed that are no longer listed, and moves blocked content into the quarantine. With
`allowed = listed` and `exclusive_targets = true`, content in an agent folder that the state
doesn't allow is quarantined too.
- **Installs** are checked by digest before anything changes: `skill add`, `skill update`,
`bundle add`, `bundle update`, `bundle override`, `project install` and restoring personal
overrides, and the same through `server serve` and `mcp serve`. Blocked content is refused;
with `allowed = listed`, so is anything the state doesn't list.
- **`skill list --check`** fails on `managed-blocked` and `managed-not-allowed`.

What this protects against:

- a state that was altered, replayed, or issued for another source or another subject, whether
it arrives over the network, from a cache, from a file or git source, or from a mirror
- a repository that tries to change the managed source, or run a command, through project
configuration: managed settings in a project file are refused, with an error naming the file
- other users on the same machine who aren't its administrator
- a user installing blocked content by mistake, directly or through a project

What it doesn't protect against:

- **The machine's administrator.**
- **The user acting deliberately.** They can copy files into an agent folder between applies,
move the clock, or start an agent without hooks. `required = true` makes following the state
the default path and refuses FastSkill's own operations until a valid state is cached. It
doesn't stop a determined user. Hard enforcement means configuring the agent itself, through
device management, to load only the folders FastSkill manages; few agents can be configured that
way today (see [Agents](#agents)).
- **Whoever holds a pinned signing key.** A key holder decides what enrolled machines install.

## How fast a block lands

A blocked digest reaches a machine at its next successful apply: at login, hourly, and at every
agent session start where a hook exists. A machine that is on and can sign in applies it within
about 70 minutes, and its agents drop the skill from their next session. A machine that stops
applying keeps what it has, however long that lasts: off, offline, or waiting for a sign-in.
Expiry limits how long FastSkill keeps allowing installs under an old state, not how long
installed content stays. The [report](/cli-reference/managed-command#the-report) tells a source
which machines haven't applied recently.

## The system file

Device management deploys the system file at a fixed location:

| Platform | Path |
| --- | --- |
| Linux | `/etc/fastskill/managed.toml` |
| macOS | `/Library/Application Support/FastSkill/managed.toml` |
| Windows | `%ProgramData%\FastSkill\managed.toml` |

FastSkill honors it only when the file and its folder are owned by the administrator account
(root, or Administrators or SYSTEM) and nobody else can write to them; otherwise it refuses to run
and names the problem. A value in the system file wins over the same value in the user file, and
no managed setting is ever read from a project, an environment variable or a flag.

```toml
source = "https://skills.example.com/state"
credential_command = ["/usr/local/bin/example-login", "token"]

[[keys]]
id = "k1"
public_key = "BASE64-ED25519-PUBLIC-KEY"

# Refuse to unenroll, and refuse commands that read or change skills until a valid state is cached.
required = true

# Optional: cover these agents instead of the ones detected for each user.
targets = ["claude", "gemini"]
```

When the system file names the source, the first apply enrolls each user: nobody has to run
`fastskill managed enroll`. With `required = true`, `fastskill managed unenroll` is refused.

## Keys

Pin the verification keys in the system file, so that no user can add one. Pin a new key before
the source signs with it, and unpin a key to revoke it: FastSkill verifies the cached state every
time it uses it, so a state signed only by an unpinned key stops counting at the next command.
Keep the private key where the machines never reach it. FastSkill never needs it; only the source
signs.

## The credential command

When the source needs a sign-in, name the credential command in the system file by absolute
path, pointing at a program only the administrator can change. It runs without a shell, and the
token it prints goes only to the source's origin. From a hook or the timer there is no terminal,
so `FASTSKILL_INTERACTIVE=0` is set and the command should fail at once instead of prompting; the
user's next `fastskill managed apply` from a terminal signs in again.

## Administrator-level hooks

`fastskill managed hooks --system` prints, for each agent that runs a command at session start,
the entry to merge into the agent's administrator-level settings. Device management places those
files; FastSkill never writes them. When an administrator-level FastSkill hook is present for an
agent, apply registers no user-level one, so there is no hook for the user to approve.

| Agent | Linux | macOS | Windows |
| --- | --- | --- | --- |
| Claude Code | `/etc/claude-code/managed-settings.json` | `/Library/Application Support/ClaudeCode/managed-settings.json` | `C:\Program Files\ClaudeCode\managed-settings.json` |
| Gemini CLI | `/etc/gemini-cli/settings.json` | `/Library/Application Support/GeminiCli/settings.json` | `C:\ProgramData\gemini-cli\settings.json` |

```bash
fastskill managed hooks --system
fastskill managed hooks --system --json
```

## The machine-wide timer

The same command prints the machine-wide timer: the files to place, and the commands to run
after placing them. When a machine-wide timer is present, apply installs no per-user one.

| Platform | What device management places |
| --- | --- |
| Linux | `/etc/systemd/user/fastskill-managed-apply.timer` and `.service`, a systemd user unit for every user |
| macOS | `/Library/LaunchAgents/dev.fastskill.managed-apply.plist`, loaded at each user's next login |
| Windows | `C:\ProgramData\fastskill\managed-apply-task.xml`, registered with `schtasks` |

Each run applies at login and then every hour, after a random wait of up to ten minutes, so a
fleet doesn't fetch all at once.

## Agents

Hard enforcement lives in the agent: one configured, through device management, to load skills
only from the folders FastSkill manages. What each agent allows, as checked against its
documentation on 2026-09-24. "Shared folder" says whether the agent reads `~/.agents/skills`;
"same session" says whether a skill written by its session-start hook shows up in that session.

| Agent | Shared folder | Command hook at session start | Administrator-level hooks | Same session |
| --- | --- | --- | --- | --- |
| Claude Code | no | yes; output reaches the model | managed settings | no |
| Cursor (IDE and CLI) | yes | yes, not awaited; output not shown | `/etc/cursor/hooks.json` | no |
| Codex CLI | yes | yes; user hooks need approval | managed hooks, no approval | later |
| Gemini CLI | yes | yes; output must be JSON | `/etc/gemini-cli/settings.json` | no |
| Qwen Code | yes | yes | `/etc/qwen-code` | later |
| GitHub Copilot (VS Code) | yes | yes; only JSON context reaches the model | GitHub policy | no |
| GitHub Copilot CLI | yes | yes; 30 s timeout | `/etc/github-copilot/policy.d` | no |
| Devin Desktop | yes | yes | `/etc/devin/hooks.json` | no |
| Auggie | yes | yes | `/etc/augment/settings.json` | no |
| Kiro, CodeBuddy, Qoder | no | yes | not checked | no |
| IBM Bob | no | yes; 10 s timeout | enforced-hooks policy | no |
| OpenCode, Amp, Pi, Kilo | yes | only through a plugin | not checked | no |
| Roo Code (archived) | yes | none | none | no |

"Later" means the agent watches its skills folders and picks the skill up during the session.
FastSkill registers session-start hooks where it has verified support (today Claude Code and
Gemini CLI); every other agent is kept up to date by the timer. Agents add hooks and folders
often, so check the agent's current documentation before relying on a row.

## Several users on one machine

The system file is machine-wide. Everything else is per user: the managed store, the machine id,
the cached state, the quarantine, the hooks and the timer. Each user enrolls and reports
separately.
3 changes: 2 additions & 1 deletion webdocs/security/meta.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"pages": [
"model",
"best-practices"
"best-practices",
"managed-machines"
],
"title": "Security"
}
Loading
Loading