You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Simlock is advisory. It works only when every agent knows the rules: lease first, never call simctl or adb directly. #147 gives those rules one printable text, simlock instructions. Putting that text where an agent tool reads it is still manual. The operator pastes it into each tool, on each machine or in each project, and again after every Simlock upgrade. A missing or stale copy is invisible until an agent breaks another agent's device.
Who it is for
An operator setting up Simlock for agents that run under Claude Code or Codex. They set up one machine for themselves, or one project so the whole team gets the rules from the repository.
Outcome
One command puts the agent rules where the agent tool reads them. From its next session on, the agent has the rules with nothing pasted by hand.
Claude Code and Codex are supported. Each gets the rules as a skill, so the tool loads them at the start of every session without the agent asking.
By default the command installs for the current user. A flag installs into the current project instead, so the files can be committed and shared.
The operator can name one tool. With none named, the command installs for every supported tool already set up on the machine or in the project, and says which it skipped.
Running the command again replaces what an earlier run wrote. An upgrade is a re-run.
The command writes only inside a directory of its own, named after Simlock, under each tool's skills directory. Everything in that directory is its own to replace. It never edits any other file.
It says what it wrote, what it replaced, and what it skipped.
Other agent tools. Tools with no skills directory keep using simlock instructions and paste.
Completion conditions
On a machine with no Simlock files, running the command for Claude Code, then for Codex, then starting each tool, gives the agent the Simlock rules as a skill without a paste. Checked by hand per tool, and by an e2e test that finds the skill at the documented location.
The default run writes only under the current user's home. With the project flag it writes only under the current directory.
With a tool named, only that tool's Simlock directory is written, and it is created if missing. With none named, only tools whose skills directory already exists are written, and the others are listed as skipped. Naming an unsupported tool is a usage error, exit 2, and writes nothing.
The rules text inside each installed skill is byte for byte what simlock instructions prints. Anything around it is the skill's own header, and nothing more.
Running the command twice leaves the same files. Running it after the rules text changed leaves only the new text.
Nothing outside the Simlock directories changes. A file inside one from an earlier run is replaced.
The output is a human view by default and lists every path written, replaced, or skipped. --json prints the same report as one JSON object.
The command never connects to or starts the daemon.
docs/CLI.md documents the command, both tools, and where each install lands, and adds it to the --json exception list in the intro. pnpm check is green.
Open questions
Decisions
None. Simlock's footprint in a tool is one directory named simlock, owned whole; that rule lives in this spec, the tests, and docs/CLI.md, not an ADR.
Technical spec
Modules touched
src/instructions/index.ts: add renderSkill(): string. Returns the SKILL.md body: YAML front matter with name: simlock and a one-sentence description (when to use Simlock before touching a simulator or emulator), a blank line, then AGENT_INSTRUCTIONS unchanged. The file ends with AGENT_INSTRUCTIONS byte for byte.
src/instructions/setup.ts (new): the one place that knows the tools. A table with two entries, claude-code and codex, each giving for the user scope (base: home directory) and the project scope (base: working directory) a presence directory and a skills directory:
The Simlock directory is <skills>/simlock, its only file SKILL.md. Exports setupAgentTools({ filesystem, homeDirectory, workingDirectory, scope, tool? }) returning a SetupReport: { scope: "user" | "project", tools: [{ tool, status: "wrote" | "replaced" | "skipped", path }] }. Two phases, so every exit leaves one state (architecture rule 12):
Inspect. For each selected tool: with tool given, it is selected; otherwise a tool is selected only when its presence directory exists, else skipped. If the Simlock directory path exists and lstat says it is not a directory (file, symlink, other), the whole run is refused before anything is written.
Write. mkdirp the Simlock directory, writeFileAtomic its SKILL.md, then rm every other entry in the Simlock directory. Status is replaced when the directory existed before, else wrote. Nothing outside the Simlock directories is touched.
Filesystem access goes through the Filesystem port only (architecture rule 9).
src/cli/index.ts: new setup command, simlock setup [--project] [--tool <claude-code|codex>] [--json]. --project selects the project scope; default is user. --tool takes exactly one of the two ids; any other value, a second --tool, or a positional is USAGE (exit 2). Never connects to or starts the daemon. Human view by default: one line per tool, <tool> <status> <path>, and for a skipped tool a hint to pass --tool; --json prints the SetupReport as one JSON object. A refused run prints nothing on stdout and fails with the CLI-level code SETUP_REFUSED (exit 2, beside USAGE in the code map), whose message names the path. CliEnvironmentPorts gains homeDirectory and workingDirectory; defaultCliEnvironment fills them from homedir() and process.cwd(), tests from strings. buildCliEnvironment exposes setupAgentTools on CliEnvironment the way it exposes writeConfigFile. Add setup to the USAGE banner.
e2e/helpers/cli.ts: CliOptions gains cwd, passed to spawn.
docs/CLI.md: ## simlock setup section: both tools, both scopes, the four paths, the report, SETUP_REFUSED; add setup to the --json exception list in the intro and SETUP_REFUSED to the exit-code table. README.md: one sentence under "Getting started" after the simlock instructions one.
Contract and event changes
None. No daemon operation, no event, no config key. SETUP_REFUSED is a CLI-level code like USAGE, not a contract error.
Rules in play
docs/internal/agent-rules/documentation.md rule 3: the skill's front matter and the command's output name no path in this repository. Paths on the operator's machine are fine.
docs/internal/agent-rules/architecture.md rules 8, 9, 12: frontend-owned, no daemon; Filesystem port only, home and cwd read at the composition root; inspect then write, so a refusal writes nothing.
docs/internal/agent-rules/safety.md rule 9, by analogy: a symlink or file where the Simlock directory should be fails closed.
docs/internal/agent-rules/testing.md: every title below is a claim; break the code and watch each fail.
Tests
Unit, src/instructions/setup.test.ts on MemoryFilesystem:
A named tool is installed even when its presence directory is missing, and the skills directory is created.
With no tool named, only tools whose presence directory exists are installed, and the others are reported as skipped.
The project scope writes under the working directory and nothing under the home directory, and the user scope the reverse.
A second run reports replaced and leaves the same files.
A stale file inside the Simlock directory from an earlier run is removed.
A file or symlink at the Simlock directory path refuses the run, and nothing is written for any tool.
Unit, src/instructions/index.test.ts:
The rendered skill ends with the exact agent instructions, and its front matter names the skill simlock.
The rendered skill names no path inside this repository (extend the existing test to cover renderSkill()).
Unit, src/cli/index.test.ts:
setup --help prints the usage line and exits 0.
setup --tool bogus, two --tool flags, and a positional each fail with USAGE, exit 2, and write nothing.
setup --json prints one JSON object with scope and one entry per tool.
A refused run fails with SETUP_REFUSED, exit 2, and prints nothing on stdout.
E2e, fast lane, e2e/setup.test.ts, with HOME and cwd pointed at temp directories:
simlock setup --tool claude-code writes .claude/skills/simlock/SKILL.md under HOME, exits 0, and starts no daemon.
simlock setup --project --tool codex writes .codex/skills/simlock/SKILL.md under the working directory and nothing under HOME.
The installed SKILL.md ends with exactly the text simlock instructions prints.
Request: #154
Problem
Simlock is advisory. It works only when every agent knows the rules: lease first, never call
simctloradbdirectly. #147 gives those rules one printable text,simlock instructions. Putting that text where an agent tool reads it is still manual. The operator pastes it into each tool, on each machine or in each project, and again after every Simlock upgrade. A missing or stale copy is invisible until an agent breaks another agent's device.Who it is for
An operator setting up Simlock for agents that run under Claude Code or Codex. They set up one machine for themselves, or one project so the whole team gets the rules from the repository.
Outcome
simlock instructionsprints. There is no second copy to keep in sync. That text comes from Addsimlock instructions: the rules an agent must follow, printable and served over MCP #147, so this feature waits for Addsimlock instructions: the rules an agent must follow, printable and served over MCP #147 to close.Non-goals
AGENTS.mdorCLAUDE.md, or a tool's settings file.simlock instructions: the rules an agent must follow, printable and served over MCP #147.simlock instructionsand paste.Completion conditions
simlock instructionsprints. Anything around it is the skill's own header, and nothing more.--jsonprints the same report as one JSON object.docs/CLI.mddocuments the command, both tools, and where each install lands, and adds it to the--jsonexception list in the intro.pnpm checkis green.Open questions
Decisions
simlock, owned whole; that rule lives in this spec, the tests, anddocs/CLI.md, not an ADR.Technical spec
Modules touched
src/instructions/index.ts: addrenderSkill(): string. Returns theSKILL.mdbody: YAML front matter withname: simlockand a one-sentencedescription(when to use Simlock before touching a simulator or emulator), a blank line, thenAGENT_INSTRUCTIONSunchanged. The file ends withAGENT_INSTRUCTIONSbyte for byte.src/instructions/setup.ts(new): the one place that knows the tools. A table with two entries,claude-codeandcodex, each giving for the user scope (base: home directory) and the project scope (base: working directory) a presence directory and a skills directory:claude-code: presence<base>/.claude, skills<base>/.claude/skillscodex: presence<base>/.codex, skills<base>/.codex/skillsThe Simlock directory is
<skills>/simlock, its only fileSKILL.md. ExportssetupAgentTools({ filesystem, homeDirectory, workingDirectory, scope, tool? })returning aSetupReport:{ scope: "user" | "project", tools: [{ tool, status: "wrote" | "replaced" | "skipped", path }] }. Two phases, so every exit leaves one state (architecture rule 12):toolgiven, it is selected; otherwise a tool is selected only when its presence directory exists, elseskipped. If the Simlock directory path exists andlstatsays it is not a directory (file, symlink, other), the whole run is refused before anything is written.mkdirpthe Simlock directory,writeFileAtomicitsSKILL.md, thenrmevery other entry in the Simlock directory. Status isreplacedwhen the directory existed before, elsewrote. Nothing outside the Simlock directories is touched.Filesystem access goes through the
Filesystemport only (architecture rule 9).src/cli/index.ts: newsetupcommand,simlock setup [--project] [--tool <claude-code|codex>] [--json].--projectselects the project scope; default is user.--tooltakes exactly one of the two ids; any other value, a second--tool, or a positional isUSAGE(exit 2). Never connects to or starts the daemon. Human view by default: one line per tool,<tool> <status> <path>, and for a skipped tool a hint to pass--tool;--jsonprints theSetupReportas one JSON object. A refused run prints nothing on stdout and fails with the CLI-level codeSETUP_REFUSED(exit 2, besideUSAGEin the code map), whose message names the path.CliEnvironmentPortsgainshomeDirectoryandworkingDirectory;defaultCliEnvironmentfills them fromhomedir()andprocess.cwd(), tests from strings.buildCliEnvironmentexposessetupAgentToolsonCliEnvironmentthe way it exposeswriteConfigFile. Addsetupto theUSAGEbanner.e2e/helpers/cli.ts:CliOptionsgainscwd, passed tospawn.docs/CLI.md:## simlock setupsection: both tools, both scopes, the four paths, the report,SETUP_REFUSED; addsetupto the--jsonexception list in the intro andSETUP_REFUSEDto the exit-code table.README.md: one sentence under "Getting started" after thesimlock instructionsone.Contract and event changes
None. No daemon operation, no event, no config key.
SETUP_REFUSEDis a CLI-level code likeUSAGE, not a contract error.Rules in play
docs/internal/agent-rules/documentation.mdrule 3: the skill's front matter and the command's output name no path in this repository. Paths on the operator's machine are fine.docs/internal/agent-rules/architecture.mdrules 8, 9, 12: frontend-owned, no daemon;Filesystemport only, home and cwd read at the composition root; inspect then write, so a refusal writes nothing.docs/internal/agent-rules/safety.mdrule 9, by analogy: a symlink or file where the Simlock directory should be fails closed.docs/internal/agent-rules/testing.md: every title below is a claim; break the code and watch each fail.Tests
Unit,
src/instructions/setup.test.tsonMemoryFilesystem:replacedand leaves the same files.Unit,
src/instructions/index.test.ts:simlock.renderSkill()).Unit,
src/cli/index.test.ts:setup --helpprints the usage line and exits 0.setup --tool bogus, two--toolflags, and a positional each fail withUSAGE, exit 2, and write nothing.setup --jsonprints one JSON object withscopeand one entry per tool.SETUP_REFUSED, exit 2, and prints nothing on stdout.E2e, fast lane,
e2e/setup.test.ts, withHOMEandcwdpointed at temp directories:simlock setup --tool claude-codewrites.claude/skills/simlock/SKILL.mdunderHOME, exits 0, and starts no daemon.simlock setup --project --tool codexwrites.codex/skills/simlock/SKILL.mdunder the working directory and nothing underHOME.SKILL.mdends with exactly the textsimlock instructionsprints.Written by an agent.