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
39 changes: 33 additions & 6 deletions 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. Implemented: 2026-10-10.
2026-10-09. Implemented: 2026-10-10. Third amendment proposed: 2026-10-17.

Related: [ADR-0001](0001-remove-sync-command.md),
[ADR-0003](0003-serve-trust-boundary-and-edge-auth.md),
Expand Down Expand Up @@ -50,6 +50,19 @@ skills and run hooks (see Agent facts, at the end):
- The credential command is told when there is no terminal, and a sign-in that lapsed is shown
instead of failing silently (decisions 6 and 16).

**Third amendment** (proposed 2026-10-17). Session-start hooks are narrowed to four agents and
widened to one that needs a plugin. The plan is
[managed session hooks](../../specs/managed-session-hooks-prd.md):

- FastSkill registers a session-start hook only for Claude Code, Codex, Cursor and Pi. Every other
agent, Gemini CLI included, is reached by the timer, and apply removes a FastSkill hook it
registered earlier for one of them (decision 16).
- Pi runs a command at session start only from an extension. `aikit-sdk` writes that extension as
a file it owns whole, which reverses the deferral of plugins in Considered alternatives.
- `managed hooks --system` prints Codex's entry as TOML for its requirements file, and says Pi has
no administrator-level location (decision 17).
- Agent facts gains rows for the four agents, checked on 2026-10-17.

## Context

team-skill-presets started from one need: a team keeps using incorrect or outdated skills, and
Expand Down Expand Up @@ -366,7 +379,8 @@ It doesn't protect against:
don't wait for it at all. What an apply deploys is guaranteed from the next session, not
the current one.
- **The session-start hook.** Each covered agent that can run a command at session start
gets a hook that runs `managed apply --hook`. It starts an apply in the background and
gets a hook that runs `managed apply --hook`. The covered agents FastSkill hooks are Claude
Code, Codex, Cursor and Pi (third amendment). It starts an apply in the background and
returns within a second. The per-user lock (decision 12) keeps applies from overlapping.
The hook always exits successfully and writes no agent metadata files.
- **What the hook prints.** Nothing, unless the last recorded apply left something the user
Expand All @@ -375,8 +389,8 @@ It doesn't protect against:
differ in whether and where they show it. `managed status` always shows it.
- **The timer.** A per-user timer runs `managed apply` at login and then hourly, each run
shifted by up to ten minutes at random: a systemd user timer, a launchd agent or a scheduled
task. It is the only path for agents that can't run a command at session start, including
agents whose hooks must be plugins in their own language, which `aikit-sdk` doesn't ship.
task. It is the only path for every other agent, whether or not it could run a command at
session start.
- **Who registers them.**
- On a machine its user manages, `managed enroll` registers user-level hooks and the timer.
For an agent that runs a new user-level hook only after its user approves it, enroll
Expand Down Expand Up @@ -550,8 +564,12 @@ It doesn't protect against:
- **Only the shared `~/.agents/skills` folder** was rejected. Claude Code doesn't read it.
- **FastSkill writing administrator-level hook files** was rejected. Those files belong to device
management, and merging into them risks breaking its other settings.
- **Plugins for agents whose hooks must be code in their own language** were deferred. The timer
reaches them, and the difference is at most an hour.
- **Plugins for agents whose hooks must be code in their own language** were deferred, and then
taken up for Pi alone by the third amendment, as an extension file `aikit-sdk` owns whole. The
timer still reaches the other agents of that kind, and the difference is at most an hour.
- **Hooks for every agent `aikit-sdk` supports** were rejected by the third amendment. Each agent
adds a file format, an approval flow and a notice to verify on real installs, and the timer
already reaches the rest.
- **Deploying into agents' administrator skill folders**, which users can't change, was rejected.
They are machine-wide, and a managed state is per user.
- **Deleting disallowed content** was rejected. Quarantine loses nothing and can be reversed.
Expand Down Expand Up @@ -588,3 +606,12 @@ shows up in that session.
| Roo Code (archived) | yes | none | none | no |

"Later" means the agent watches its skills folders and picks the skill up during the session.

Rechecked for the four hooked agents on 2026-10-17 (third amendment):

| Agent | User hook file | Hook event and output | Approval | Administrator-level hooks |
|---|---|---|---|---|
| Claude Code | `~/.claude/settings.json` | `SessionStart`; `systemMessage` is shown | review in `/hooks` | managed settings |
| Codex CLI | `~/.codex/hooks.json` | `SessionStart`, matcher `startup\|resume`; `systemMessage` is shown as a warning, plain output reaches the model | trust in `/hooks`, again whenever the hook changes | `[hooks]` in `requirements.toml`, trusted by policy |
| Cursor | `~/.cursor/hooks.json` | `sessionStart`, not awaited; only `additional_context` reaches the model, nothing is shown | none | `/etc/cursor/hooks.json` and the team dashboard |
| Pi | an extension in `~/.pi/agent/extensions` | `session_start` extension event, awaited; the extension shows a notice with `ctx.ui.notify` | none | none |
175 changes: 175 additions & 0 deletions specs/managed-session-hooks-prd.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# PRD: Managed session hooks for Claude Code, Codex, Cursor and Pi

Status: proposed.
Date: 2026-10-17.

This PRD plans the third amendment of
[ADR-0016](../docs/adr/0016-machines-follow-a-signed-managed-state.md) (decisions 16, 17 and 20).
It depends on the `aikit-sdk` change planned in aikit's
`docs/plans/session-hooks-codex-cursor-pi.md` and decided in aikit ADR 0025. MUST, MUST NOT,
SHOULD and MAY express requirement strength.

## Problem

`managed enroll` and every `managed apply` register a session-start hook that starts a background
apply, for each covered agent `aikit-sdk` can hook. Today that is Claude Code and Gemini CLI. The
agents people on managed machines use are Claude Code, Codex, Cursor and Pi. For Codex, Cursor
and Pi, a new managed state arrives only from the timer, up to an hour later, and a lapsed sign-in
shows only in `managed status`, which nobody runs unprompted.

## Solution

FastSkill hooks exactly four agents: Claude Code, Codex, Cursor and Pi. Every other agent is
reached by the timer alone. A user on a machine they manage runs `managed enroll` once, approves
the hook where the agent asks, and from then on each new session in any of the four agents starts
an apply and shows a notice when they must act. Device management gets the administrator-level
entry for each of the four agents that has one from `managed hooks --system`.

## User stories

1. As a user of Codex, I want each new or resumed session to start a managed apply, so that a
changed managed state is in place by my next session instead of within an hour.
2. As a user of Cursor, I want each new conversation to start a managed apply, so that Cursor
picks up managed skills as soon as the other agents do.
3. As a user of Pi, I want each started, new or resumed session to start a managed apply, so that
Pi isn't left on the timer only.
4. As a user of Claude Code, I want my hook to keep working unchanged, so that this change costs
me nothing.
5. As a user, I want the hook to add no wait to my session start, so that managed skills never
slow me down.
6. As a user, I want the hook to cost no model tokens when nothing needs me, so that it is free in
normal use.
7. As a user whose sign-in lapsed, I want a notice in the agent I'm using, so that I know to run
`fastskill managed apply` in a terminal.
8. As a Codex user, I want `managed enroll` to tell me that Codex runs the hook only after I trust
it in `/hooks`, so that I don't think the hook is broken.
9. As a Codex user, I want to be told again when the hook changes and needs trust again, so that
an upgrade that moves the program doesn't silently stop the hook.
10. As a Cursor user, I want to know that Cursor doesn't show me the notice, so that I check
`managed status` when skills seem stale.
11. As a Pi user, I want the notice shown in Pi's interface and kept out of the model's context,
so that it costs no tokens.
12. As a Pi user running Pi without an interface, I want the hook to stay silent, so that print
and RPC output aren't polluted.
13. As a Gemini CLI user enrolled before this change, I want the FastSkill hook removed from my
Gemini settings on the next apply, so that nothing is left behind that FastSkill no longer
maintains.
14. As a Gemini CLI user, I want the timer to keep my skills current, so that dropping the hook
loses at most an hour.
15. As a user with my own hooks in an agent's file, I want FastSkill to leave them exactly as they
were, so that enrolling never breaks my setup.
16. As a user with a Pi extension that happens to have FastSkill's file name, I want FastSkill to
refuse and leave it alone, so that my code is never overwritten or deleted.
17. As a Codex user who keeps hooks in `config.toml`, I want FastSkill to warn me and write
nothing, so that Codex doesn't warn about hooks in two files.
18. As a user, I want `managed hooks` to list the four agents and whether each hook is registered,
so that I can see what runs at session start.
19. As a user, I want `managed unenroll` to remove the hook from all four agents, so that nothing
of FastSkill stays behind.
20. As an administrator, I want `managed hooks --system` to print Codex's entry as TOML for
`requirements.toml`, so that Codex trusts it by policy and users approve nothing.
21. As an administrator, I want `managed hooks --system` to print Cursor's entry for its
administrator-level `hooks.json`, so that I can place it with device management.
22. As an administrator, I want `managed hooks --system` to say Pi has no administrator-level
location and the timer covers it, so that I don't look for one.
23. As an administrator, I want apply to register no user-level hook for an agent whose
administrator-level FastSkill hook is present, so that the hook doesn't run twice.
24. As a user, I want `managed hooks --json` to carry the same facts, so that scripts can check
them.
25. As a maintainer, I want agent file formats to live only in `aikit-sdk`, so that FastSkill
knows nothing about how each agent stores hooks.

## Implementation decisions

### The hooked agents

- FastSkill MUST keep its own list of hooked agents: Claude Code, Codex, Cursor and Pi, by their
`aikit-sdk` keys.
- Hook sync MUST register the hook only for an agent that is in that list, supported by
`aikit-sdk`, and covered by the current apply, unless an administrator-level FastSkill hook is
present for it.
- Hook sync MUST remove FastSkill's hook from every other agent `aikit-sdk` supports, Gemini CLI
included. This is how existing Gemini hooks go away, so `aikit-sdk` keeps its Gemini row.
- `managed unenroll` MUST remove FastSkill's hook from every agent `aikit-sdk` supports, not only
from the four.
- `managed hooks` and `managed hooks --system` list the four agents only.

### The hook itself

- The hook MUST be built from an argument vector: the absolute path of the running FastSkill
program, then `managed`, `apply`, `--hook` and the hook id `fastskill-managed-<agent>`. The
shell command form other agents store is derived from it by `aikit-sdk`.
- What `managed apply --hook` does is unchanged: it starts a detached apply, returns at once,
always exits successfully, and prints at most one notice through `aikit-sdk`'s notice
formatting for the agent named in the hook id.
- Pi passes the notice to its interface only when one exists, so print, JSON and RPC modes stay
clean.

### Enroll and apply messages

- When a hook is newly added, enroll and apply MUST print the agent's approval note when
`aikit-sdk` gives one: Claude Code and Codex.
- When `aikit-sdk` reports the Codex hook as changed rather than unchanged, the note MUST be
printed again, because Codex asks for trust again.
- A refusal from `aikit-sdk` (a file of the wrong shape, a Codex `config.toml` with hooks, a Pi
file FastSkill didn't write) MUST become a warning in the apply result, never a failed apply.

### Administrator-level entries

- `managed hooks --system` MUST print each entry in the format `aikit-sdk` gives: JSON for Claude
Code and Cursor, TOML for Codex, with the file it belongs in.
- For Pi it MUST print that there is no administrator-level location and that the machine-wide
timer covers Pi.
- `managed hooks --system --json` carries each entry's format and text as strings.

### Documentation

- The CLI reference for `managed` and the managed-machines security page list the four agents,
each one's approval step, where its notice shows, and its administrator-level location.
- Gemini CLI is listed as reached by the timer.

### Known gaps

- Pi installs that move the agent folder with `PI_CODING_AGENT_DIR` get no hook; the timer still
reaches them.
- Whether Codex's IDE extension and app run hooks is not documented. Only Codex CLI is claimed.
- Cursor shows the notice to the model, not the user.

## Testing decisions

- Good tests check what the commands do to a temporary home directory and what they print, not
how the code is arranged.
- Hook sync is tested at its existing seam, the hook tests in `fastskill-core`'s managed module:
the four agents are registered when covered, Gemini's existing hook is removed, an agent outside
the list is never registered, an administrator-level hook suppresses the user-level one, and a
refusal becomes a warning.
- `managed hooks`, `managed hooks --system` and `managed apply --hook` are tested at the existing
CLI seam, the managed hooks tests in `fastskill-cli`: the four agents listed, Codex's TOML
entry, Pi's "no location" line, and the notice form for each agent.
- The approval-note rule is tested for added and changed Codex hooks.
- The file formats themselves are `aikit-sdk`'s to test.

## Verification

Before release, on real installs of each of the four agents, record the agent version and check:

1. `managed enroll` registers the hook and prints the approval step where there is one;
2. after approval, a new session and a resumed session each start an apply (the last-apply time in
`managed status` moves);
3. with the sign-in lapsed, the notice shows where this PRD says it does;
4. `managed unenroll` removes the hook;
5. on a machine enrolled before this change with Gemini CLI, the next apply removes the Gemini
hook.

## Out of scope

- Hooks for any agent other than the four, including GitHub Copilot and Gemini CLI.
- FastSkill writing administrator-level files.
- Making a skill visible in the session whose hook deployed it.
- Any change to the managed state, its verification or the timer.

## Further notes

Sources checked on 2026-10-17: Cursor's hooks documentation, OpenAI's Codex hooks and managed
configuration documentation, and the documentation and source of Pi's coding agent
(`badlogic/pi-mono`, `packages/coding-agent`).
Loading