Repository navigation
One command installs the agent rules where agent tools read them #620
Description
Part of #157.
Scope
An operator runs simlock setup once and every later Claude Code or Codex session has a small simlock skill. The skill tells the agent what Simlock is, when to use it, to check its setup with simlock whoami, and to run simlock instructions for the rules. In a terminal, setup asks for the scope and, per tool, whether to connect Simlock's MCP server too; flags answer both for scripts.
Start from origin/feature/157 commits d990d7d3f, ee5262d58, 0fdcdc96d: src/instructions/setup.ts and its tests carry over. Change the Codex paths, replace the copied rules with the router, add the prompts and the MCP step, and re-port the CLI part to src/cli/commands/ (#147 F2).
The task is about 10 Tests lines; it stays one PR because the prompts, the MCP step and the report share one report shape and one command.
In short
simlock setup writes SKILL.md into a simlock directory under each tool's skills directory, for the user (home) or the project (current directory). The skill holds no rules, so it stays right after an upgrade. Simlock's MCP server is connected only after a yes or --mcp, by running the tool's own mcp add command. If it goes wrong, an agent starts without the skill, or a file outside the simlock directory changes.
flowchart LR
O[Operator] --> S[simlock setup]:::changed
S -->|asks in a terminal| P[scope, MCP per tool]:::changed
S -->|writes| K[skills/simlock/SKILL.md]:::changed
S -->|on yes or --mcp| C[claude/codex mcp add]:::changed
A[Next agent session] -->|loads| K
A -->|runs| I[simlock instructions]
classDef changed stroke-width:3px
Session, after (on a machine with ~/.claude and no ~/.codex):
$ simlock setup
Install only for this project (not every project)? [y/N]
Also connect Simlock's MCP server to Claude Code? [y/N] y
claude-code skill wrote /Users/me/.claude/skills/simlock
claude-code mcp connected
codex skill skipped /Users/me/.agents/skills/simlock (Codex not found here; pass --tool codex to install anyway)
$ simlock setup --project --tool codex --mcp --json
{"scope":"project","tools":[{"tool":"codex","skill":{"status":"wrote","path":"/work/app/.agents/skills/simlock"},"mcp":{"status":"skipped","reason":"codex mcp add has no project scope"}}]}
Before, every one of these is {"error":{"code":"USAGE","message":"Unknown command: setup (run simlock --help for usage)"}}, exit 2 (#147 F1).
| Input | After |
|---|---|
simlock setup in a terminal, both prompts default |
user scope, tools found, no MCP |
simlock setup with no terminal |
user scope, tools found, no MCP, no prompt |
simlock setup --project |
project scope, no scope prompt |
simlock setup --user --project |
USAGE, exit 2, nothing written |
simlock setup --tool codex with no ~/.codex |
Codex installed, directories created |
simlock setup --tool cursor / two --tool / a positional |
USAGE, exit 2, nothing written |
simlock setup --mcp --no-mcp |
USAGE, exit 2, nothing written |
simlock setup --mcp, claude not on PATH |
skill written, mcp skipped (claude not found), exit 0 |
simlock setup --mcp, claude mcp add fails |
skill written, mcp failed (<its first stderr line>), then SETUP_MCP_FAILED, exit 1 |
simlock setup --json in a terminal |
no prompts: scope user unless --project, MCP only with --mcp |
a file or symlink at ~/.claude/skills/simlock |
SETUP_REFUSED, exit 2, nothing written, stdout empty |
Exact strings:
- Scope prompt, on stderr:
Install only for this project (not every project)? [y/N] - MCP prompt, on stderr:
Also connect Simlock's MCP server to Claude Code? [y/N], and for CodexAlso connect Simlock's MCP server to Codex? Codex counts it as a second agent beside the CLI. [y/N] - Refusal message:
Refusing to install: <path> exists and is not a directory. Nothing was written. Move it aside and run setup again. - MCP failure message:
MCP not connected for <tool>: <reason>, one per failed tool, joined by;. - Report lines: the tool id padded to 11 characters, two spaces, the step (
skillormcp) padded to 5, two spaces, then the status; forskillthe status is padded to 9 (the length ofconnected), then two spaces and the path. Soclaude-code skill wrote <path>andcodex mcp connected. A skipped skill ends with(<Tool name> not found here; pass --tool <tool> to install anyway). Themcpline (statusesconnected,already,failed,skipped) follows its tool's skill line only when MCP was asked for, and ends with(<reason>)afterfailedandskipped. Tool names:Claude Code,Codex. - MCP is asked for when
--mcpis passed or the MCP prompt is answered yes. At project scope Codex gets no MCP prompt; with--mcpit reportsskipped(codex mcp add has no project scope).
The router skill
SKILL.md is YAML front matter, name: simlock and a description of at most 1024 characters saying when to use it (before touching an iOS simulator, Android emulator or test phone, or running simctl, adb, emulator, xcodebuild for a simulator, or agent-device), then a short body:
- One paragraph on what Simlock is: it shares simulators, emulators and phones between agents, and works only if every agent follows its rules.
- Check it is set up: run
simlock whoami. Ifsimlockis not found, orwhoamifails, tell the user, and do not touch any device. - Get the rules: run
simlock instructionsand follow them. They match the installed Simlock, so read them again in each session instead of relying on memory. - The last line:
More: https://callstackincubator.github.io/simlock/docs/getting-started/connect-your-agent/
It holds none of #147's section headings and no other rule.
Technical spec
Facts
Commit cac23e033 unless noted.
| # | Fact | Source |
|---|---|---|
| F1 | Claude Code loads skills from ~/.claude/skills/<name>/SKILL.md (user, symlinks followed) and <project>/.claude/skills/<name>/SKILL.md |
This session (Claude Code 2.1.304): ~/.claude/skills/caveman is a symlink to ../../.agents/skills/caveman (ls ~/.claude/skills) and caveman is in the session's skill list; .claude/skills/spec-session/SKILL.md in this repo loaded as the spec-session skill |
| F2 | Codex 0.163.0 reads user skills from ~/.agents/skills (home directory, not CODEX_HOME) and still from the deprecated $CODEX_HOME/skills; repo skills from .agents/skills in each directory from the project root to cwd |
codex --version → codex-cli 0.163.0; openai/codex rust-v0.163.0 codex-rs/ext/skills/src/host_roots.rs:24-25 ".agents", "skills"; :36 home_dir(); :95-108 "Deprecated user skills location"; :148-173 .agents/skills per directory; codex-rs/utils/home-dir/src/lib.rs:6-7 the Codex home is CODEX_HOME, default ~/.codex |
| F3 | Codex requires front matter opening with --- and a non-empty description; name at most 64 characters |
rust-v0.163.0 codex-rs/skills/src/parser.rs:4 MAX_NAME_LEN: usize = 64, :83-85 MissingField("description"), :200-202 |
| F4 | claude mcp add takes -s, --scope <local|user|project>; codex mcp add <NAME> -- <COMMAND>... has no scope option |
claude mcp add --help (2.1.304) line -s, --scope <scope> Configuration scope (local, user, or project); codex mcp add --help (0.163.0): options -c, --env, --enable, --url, --bearer-token-env-var only |
| F5 | claude mcp add -s <scope> exits 0 the first time and exits 1 with MCP server simlock already exists in <user config|.mcp.json> when that scope has it; -s project writes .mcp.json in the cwd; codex mcp add writes $CODEX_HOME/config.toml and a second add overwrites, exit 0 |
Temp HOME, Claude Code 2.1.304: claude mcp add -s user simlock -- simlock mcp → Added stdio MCP server simlock ... to user config, exit 0; again → MCP server simlock already exists in user config, exit 1; -s project in a temp dir → .mcp.json written, exit 0; again → already exists in .mcp.json, exit 1. Codex 0.163.0 with temp CODEX_HOME: codex mcp add simlock -- simlock mcp twice → Added global MCP server 'simlock'., exit 0 both times. (claude mcp get is not used: it answers for every scope.) |
| F6 | The CLI's confirm port asks a [y/N] question on stderr and answers false with no terminal |
src/cli/environment.ts:360 confirm: ports.confirm ?? confirmTerminal, :364-371 if (!process.stdin.isTTY) return false;; src/cli/types.ts:82, :99 |
| F7 | Filesystem has mkdirp, writeFileAtomic, rm, lstat, readdir, exists, stat; MemoryFilesystem fakes it |
src/ports/filesystem.ts:59-86, :305 |
| F8 | ProcessRunner.run(command, args, { timeoutMs, cwd }) returns { code, stdout, stderr }, code null when the child was killed by a signal, as after timeoutMs; only NodeProcessRunner throws ProcessSpawnError for a missing binary; ScriptedProcessRunner can model neither |
src/ports/process-runner.ts:33-36, :61-65, :151, :196-235 (timeout sends SIGTERM, then SIGKILL), :251; :636-660 expectation fields |
| F9 | CliEnvironment has no home directory, working directory, environment variables or process runner; the composition root reads homedir() |
src/cli/types.ts:52-84; src/cli/index.ts:145 home: homedir(), |
| F10 | A CLI-level error code is an error class checked by name then instanceof in cliErrorCode/errorExitCode, like SERVICE_FAILED |
src/cli/errors.ts:17-22, :36-51 |
| F11 | The e2e CLI helper takes env, input, timeout, entry, no cwd; it spawns with { env: { ...env, ...options.env } } |
e2e/helpers/cli.ts:9-14, :40-42 |
| F12 | The earlier build: inspect every tool, then write; refuse a non-directory at the skill path; skip a file posing as a tool directory; replace a SKILL.md that is a directory; remove other entries |
origin/feature/157:src/instructions/setup.ts:61-140 |
| F13 | GATEWAY_EXEMPT_PATHS holds daemon start/install/uninstall, config set, mcp, and instructions after #147 |
src/cli/index.ts:160-166; #147 Modules touched |
| F14 | simlock mcp is the MCP server command |
src/cli/tree.ts:131; website/src/content/docs/docs/getting-started/connect-your-agent.mdx:105 codex mcp add simlock -- simlock mcp |
Values sent to other tools:
| Value | To | Passes because |
|---|---|---|
claude mcp add -s user simlock -- simlock mcp, -s project for the project scope, run with cwd = working directory |
Claude Code | F4: -s takes user and project; <name> <commandOrUrl> [args...] |
codex mcp add simlock -- simlock mcp |
Codex | F4; F5: a second add overwrites and exits 0 |
name: simlock |
both, front matter | F3: 7 characters ≤ 64; F1: matches the directory name |
Modules touched
Searches: setupAgentTools, renderSkill, SETUP_REFUSED (0 hits on main); confirm?.( (4 commands); GATEWAY_EXEMPT_PATHS; homedir( in src/cli.
src/instructions/index.ts:renderSkill(): string, the router skill above.src/instructions/setup.ts(new, from the branch): the tool table (claude-code: presence.claude, skills.claude/skills;codex: presence.codex(user scope:$CODEX_HOMEwhen set), skills.agents/skills; all under the scope's base),setupAgentTools({ filesystem, runTool, confirm, homeDirectory, workingDirectory, codexHome?, scope?, tool?, mcp?, interactive })→SetupReport.runTool(bin, args, { cwd })resolves{ outcome: "exited", code, stderr } | { outcome: "not-found" } | { outcome: "timed-out" };toolRunner(processRunner)builds it overProcessRunner.runwithtimeoutMs: 30_000:ProcessSpawnError→not-found,code === null→timed-out(F8). The unit tests fakerunToolwith a plain function, and testtoolRunnerwith a hand-writtenProcessRunnerstub. Steps: scope (flag, elseconfirmwheninteractive, else user); inspect every selected tool and refuse before any write (F12); write; then MCP per written tool, one at a time.SetupRefusedError,SetupMcpFailedError(carries the report).src/cli/tree.ts,src/cli/commands/setup.ts(new):simlock setup [--user | --project] [--tool <claude-code|codex>] [--mcp | --no-mcp] [--json].interactiveis false with--json. Prints the report, then onSetupMcpFailedErrorthe error line.src/cli/types.ts,src/cli/environment.ts:CliEnvironmentPortsandCliEnvironmentgainhomeDirectory,workingDirectory,codexHomeandrunTool, filled at the composition root fromhomedir(),process.cwd(),process.env.CODEX_HOMEandtoolRunner(new NodeProcessRunner())(loaded on demand), from strings and a fake function in tests.src/cli/errors.ts:SETUP_REFUSED(exit 2) andSETUP_MCP_FAILED(exit 1), checked lazily likeSERVICE_FAILED(F10).src/cli/index.ts:setupinUSAGEandGATEWAY_EXEMPT_PATHS.src/instructions/AGENTS.md,src/cli/AGENTS.md: the setup step order, the tool table, the exempt list.e2e/helpers/cli.ts:CliOptions.cwd, passed tospawn.- Tests:
src/instructions/setup.test.ts,src/instructions/index.test.ts,src/cli/index.test.ts,e2e/setup.test.ts. - Website:
reference/cli.mdx(row,## simlock setupsection: flags, prompts, both tools, both scopes, the four skill paths, the report, MCP commands;setupadded to the exempt list, worded without a count),reference/errors.mdx(SETUP_REFUSED,SETUP_MCP_FAILED),getting-started/connect-your-agent.mdx(first step:simlock setup; the MCP tabs mentionsetup --mcp).
flowchart LR
CMD[cli/commands/setup]:::changed --> SET[instructions/setup]:::changed
SET --> IDX[instructions/index renderSkill]:::changed
SET --> FS[ports Filesystem]
SET --> PR[ports ProcessRunner]
CMD --> ENV[cli environment: home, cwd, runner, confirm]:::changed
classDef changed stroke-width:3px
sequenceDiagram
participant S as simlock setup
participant C as claude (or codex)
S->>C: mcp add [-s user|project] simlock -- simlock mcp (30 s)
alt exit 0
C-->>S: mcp connected
else claude exit 1, "already exists in"
C-->>S: mcp already
else not on PATH
C-->>S: mcp skipped "claude not found"
else other exit, or no answer in 30 s
C-->>S: mcp failed "<first stderr line>" or "timed out after 30 s" → SETUP_MCP_FAILED, exit 1
end
Surface and extension points
src/instructions grows setupAgentTools, a function taking its ports; no handle. The tool table is the one place that knows the tools; a third tool is a row, and no extension point is added (architecture rule 19: none is named).
Contract and event changes
None after a search of src/contract and docs/internal/EVENTS.md: no daemon operation, event or config key. SETUP_REFUSED and SETUP_MCP_FAILED are CLI-level codes, not contract errors. SetupReport:
{ scope: "user" | "project", tools: [{ tool: "claude-code" | "codex", skill: { status: "wrote" | "replaced" | "skipped", path: string }, mcp?: { status: "connected" | "already" | "failed" | "skipped", reason?: string } }] }. mcp is absent when MCP was not asked for or the answer was no. reason is set for failed and skipped, at most the first line of the tool's stderr, cut to 200 characters.
Other code on the same state
Searched \.claude/skills, \.agents/skills, mcp add in src (0 hits). No other Simlock code writes these directories or the tools' MCP config. A second setup at once on the same scope: both write the same bytes with writeFileAtomic, last rename wins, same result; not tested (two operators running setup at once is not a real case).
Failure modes
| Call | Outcome | What happens | Limit, counted from |
|---|---|---|---|
Filesystem inspect |
a non-directory at the skill path | SETUP_REFUSED, nothing written for any tool |
none (local) |
Filesystem write |
throws (permissions, disk full) | INTERNAL, exit 1; skills already written stay; re-running finishes |
none (local) |
runTool(<bin>, mcp add ...) |
exit 0 | connected |
30 s from spawn |
Claude Code exit ≠ 0, stderr holds already exists in |
already (F5) |
30 s from spawn | |
| other exit ≠ 0 | failed, reason its first stderr line; SETUP_MCP_FAILED after the report |
30 s from spawn | |
not on PATH (not-found) |
skipped, reason <bin> not found |
— | |
no answer (timed-out) |
failed, reason timed out after 30 s |
30 s from spawn |
State written: the skill directories. An interrupted run leaves some tools' SKILL.md written and others not; each file is whole (writeFileAtomic), and a re-run reaches the same end state. MCP config is written by the tool itself.
Rules in play
- Documentation rule 3, "Nothing the running tool prints ... names a file path in this repository": the skill points only at the website URL; paths on the operator's machine are fine.
- Documentation rule 9: the three pages above.
- Architecture rule 9, "Every external API is accessed through a port interface": files through
Filesystem, the tools throughrunTooloverProcessRunner, the prompt throughconfirm; home and cwd read only at the composition root. - Architecture rule 11, "Every wait that crosses a process boundary is bounded": 30 s per tool command.
- Architecture rule 12, "Every exit from a method leaves its subject in exactly one named state": inspect before write, so a refusal writes nothing.
src/cli/AGENTS.md, router files "import no component for a value": the process runner is loaded on demand.
Tests
Seams: src/instructions/setup.test.ts (model origin/feature/157:src/instructions/setup.test.ts, on MemoryFilesystem, a fake runTool function and a fake confirm), src/instructions/index.test.ts, src/cli/index.test.ts, and e2e/setup.test.ts (fast lane, model origin/feature/157:e2e/setup.test.ts).
- An operator runs
setupand gets the skill in the right place for the scope: user →<home>/.claude/skills/simlock/SKILL.mdand<home>/.agents/skills/simlock/SKILL.md, nothing under cwd; project → the same under cwd, nothing under home. setuppicks tools: none named → only tools whose presence directory exists, the othersskipped; a file named.claude→skipped;codexHomeset to a directory that exists and no~/.codex→ Codex installed at user scope;--tool codexwith no.codex→ installed, directories created.setupasks in a terminal and never blocks a script: scope prompt yes → project; default → user;--user/--project→ no scope prompt; not interactive → no prompt, user, no MCP; MCP prompt yes → MCP step runs; default → nomcpin the report; Codex at project scope → no MCP prompt. The fakeconfirmrecords each question, asserted against the exact prompt strings above.setupre-run replaces only its own files: second run →replaced, same bytes; a stale file in the skill directory → removed; aSKILL.mdthat is a directory → replaced by the file.setuprefuses a file or symlink where the skill directory goes: a file; a symlink; a refusal in the second tool → nothing written for the first; via the CLI →SETUP_REFUSED, exit 2, stdout empty, the message exactly as above.setupconnects MCP through the tool's command:addexit 0 →connected, with the exact arguments andcwdof the Values table for each tool and scope; Claude Code exit 1 withMCP server simlock already exists in user config→already; other exit 1 →failedwith its first stderr line, cut to 200 characters;timed-out→failed,timed out after 30 s;not-found→skipped,claude not found; Codex at project scope with--mcp→skipped,codex mcp add has no project scope, nothing run;--no-mcp→ nothing run;toolRunnerover a stubProcessRunner: throwsProcessSpawnError→not-found,code: null→timed-out,code: 1→exited, and it passestimeoutMs: 30000.- An agent reads the router skill: front matter opens with
---,name: simlock, a non-emptydescription≤ 1024 characters; the body namessimlock whoamiandsimlock instructions; it holds none of Addsimlock instructions: the rules an agent must follow, printable and served over MCP #147's section headings; the last line is the website URL; no path in this repository (extend Addsimlock instructions: the rules an agent must follow, printable and served over MCP #147's test). setupprints its report: the report lines exactly as in Exact strings, for a written, a replaced and a skipped skill and eachmcpstatus;--json→ oneSetupReportobject; an MCP failure → the report on stdout, thenSETUP_MCP_FAILEDnaming the tool, exit 1;--tool bogus, two--tool, a positional,--mcp --no-mcp,--user --project→USAGE, exit 2, nothing written;--gatewaywith no gateway → runs;--help→ the usage line, exit 0.- An operator runs
simlock setupfor real and the skill lands where the tool reads it: temp HOME, a fakeclaudescript first onPATHthat records its arguments and exits 0;setup --tool claude-code --mcp→SKILL.mdunder HOME equal torenderSkill(), the fake gotmcp add -s user simlock -- simlock mcp, exit 0, no daemon socket;setup --tool codex --mcpwith nocodexonPATH→ skill written,mcp skipped, exit 0;setup --project --tool codex --no-mcpwithcwda temp directory →.agents/skills/simlock/SKILL.mdunder it, nothing under HOME.
Done when
pnpm test:e2e e2e/setup.test.tspasses: the skill lands at the documented place for each scope, and MCP is connected through the tool's command.- By hand, on a machine with no Simlock files:
simlock setup --tool claude-code, then--tool codex, then starting each tool lists thesimlockskill, and asking the agent to use a simulator makes it runsimlock whoamiandsimlock instructions. simlock --helplistssetup.reference/cli.mdx,reference/errors.mdxandconnect-your-agent.mdxdescribe the command, both tools, both scopes, the paths and the two error codes.pnpm checkis green.
Out of scope
- Editing a file the operator owns (
AGENTS.md,CLAUDE.md, a tool's settings or MCP config by hand). - A dry run or an uninstall.
- Agent tools other than Claude Code and Codex.
- Codex MCP at project scope (F4).
- Changing the rules text: Add
simlock instructions: the rules an agent must follow, printable and served over MCP #147.
Depends on
Written by an agent.
Activity
chore(agents): one spec session, explored first, tasks written from t…
V3RON commentedon Oct 23, 2026
Spec updated: router skill instead of a copy, Codex paths moved to
.agents/skills, terminal prompts for scope and MCP, MCP connect throughclaude/codex mcp add, website pages; Facts, Failure modes and Tests added.Written by an agent.
test: setup writes the skill where each tool reads it for the scope (#…
feat(instructions): setup writes under home or the working directory …
test: setup picks the tools set up here, and installs a named tool an…
feat(instructions): setup installs the tools set up here (#620)
test: setup connects MCP through the tool's own command (#620)
feat(instructions): setup connects MCP through the tool's own command (…
test: setup asks in a terminal and never blocks a script (#620)
feat(instructions): setup asks for the scope and for MCP in a terminal (
5 remaining items
fix(instructions): the setup tests walk the root directory (#620)
test: an agent reads the router skill (#620)
feat(instructions): the router skill sends the agent to whoami and in…
test: setup prints its report and fails the way the spec says (#620)
feat(cli): simlock setup prints its report and reports its two failur…
test: setup connects MCP through the tool's command in a real run (#620)
docs(instructions): simlock setup on the website and in the manuals (#…
refactor(instructions): split setup into steps for Fallow (#620)
refactor(instructions): smaller setup steps (#620)
test(instructions): the skill description names the command list as t…
fix(instructions): the CLI reads the tool table; a test pins project-…
docs(instructions): the manual names no file that does not exist (#620)
feat(cli): simlock instructions prints the rules an agent must follow (…
feat(cli): simlock setup installs the Simlock skill into Claude Code …