lshed 0.4.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +247 -72
  3. package/dist/cli.js +462 -205
  4. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.0 — 2026-09-03
4
+
5
+ `settings.json` travels, without merging.
6
+
7
+ - New category `settings` for Claude Code: each top-level key of `~/.claude/settings.json` (`hooks`, `permissions`, `env`, `model`, `theme`, …) is one component, stored as `settings/<key>.json`. `restore` writes only the keys the profile lists; a profile can carry `permissions` and leave `model` to each machine. `enabledPlugins` is skipped because the plugin packages own it.
8
+ - Absolute paths under the home directory are stored as `${HOME}/…` (MCP entries too), so hook commands survive a different home. Claude Code does not expand variables in `settings.json`, so `restore` expands `${HOME}` and other `${VAR}` there from the shell; MCP placeholders are still left for Claude Code.
9
+ - `env` in settings is a secret map: secret-looking keys are masked.
10
+ - The secret heuristic now matches whole words. `CLAUDE_CODE_MAX_OUTPUT_TOKENS` is not a token; `BYPASS_PERMISSIONS` is not a password.
11
+ - `init` and `add` flag an entry whose value points inside a package (a hook a toolkit's installer wrote) and suggest `exclude:`.
12
+ - The MCP adapter became a generic "one JSON key = one entry" store used by both categories.
13
+
14
+ ## 0.6.0 — 2026-09-03
15
+
16
+ - `lshed sync [-m <msg>] [--no-push] [--dry-run]`: commits everything in the shed, `git pull --rebase`, `git push` (setting the upstream the first time). Without `origin` it only commits. When commits come in it says to run `lshed restore`. On a conflict it aborts the rebase, leaves your commit in place and hands you the git command. It warns first if `diff` shows edits you have not saved, and never runs `save` for you.
17
+ - README rewritten as a usage guide: day one, daily loop, new machine, profiles, adding things, updating packages, housekeeping, output marks, every command with its flags.
18
+
19
+ ## 0.5.0 — 2026-09-03
20
+
21
+ `init` was a one-shot. Anything you made afterwards had to be copied into the shed and typed into `lshed.yaml` by hand.
22
+
23
+ - `lshed add [keys...] [--all]` scans the agent root the way `init` does and lists what the shed lacks: authored skills, agents, commands, MCP servers, and git clones or plugins that should be packages. Without keys it only lists. Chosen items are copied (or recorded with a lock entry for packages), appended to `lshed.yaml` with your comments intact, added to the current profile, and added to the managed set.
24
+ - `status` reports things outside the shed as `창고 밖 N개 → lshed add`.
25
+ - `init --exclude` is now remembered as `exclude:` in the manifest, so `add` and `status` do not keep proposing the aliases you left out.
26
+ - `init` and `add` share one classification path (`discover`) and one ingest path, so they cannot drift apart. `init` now edits a YAML document instead of serialising an object; output is unchanged.
27
+
3
28
  ## 0.4.0 — 2026-09-03
4
29
 
5
30
  Hand-configured MCP servers travel with the profile. Secret values do not.
package/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # lshed
2
2
 
3
- Keep your coding-agent harness — skills, subagents, commands, instructions — in a **shed**, and restore it on any machine with one command.
3
+ Keep your coding-agent harness — skills, subagents, commands, instructions, MCP servers, settings — in a **shed**, and restore it on any machine with one command.
4
4
 
5
5
  ```
6
6
  lshed init --shed ~/lshed # scan ~/.claude into a shed + write lshed.yaml
7
7
  lshed restore research # apply a profile anywhere
8
8
  ```
9
9
 
10
- The shed is a plain directory. Put it in a git repo, Dropbox, whatever. lshed does not sync it for you.
10
+ The shed is a plain directory. Put it in a git repo, Dropbox, whatever. `lshed sync` wraps the git part if you want it to.
11
11
 
12
12
  ## Why
13
13
 
@@ -17,7 +17,7 @@ lshed adds three first-class ideas on top of "a directory in git":
17
17
 
18
18
  | Idea | What it gives you |
19
19
  |---|---|
20
- | **Components** | every skill / agent / command / instruction fragment is one named part in the shed |
20
+ | **Components** | every skill / agent / command / instruction fragment / MCP server / settings key is one named part in the shed |
21
21
  | **Profiles** | named recipes — `research`, `work`, `minimal` — that pick a subset of parts |
22
22
  | **Managed set** | lshed remembers what it placed, so switching profiles removes only its own files and never touches yours |
23
23
 
@@ -29,29 +29,190 @@ Currently supports **Claude Code** (`~/.claude`). Other agents plug in through a
29
29
  npm install -g lshed
30
30
  ```
31
31
 
32
- Node 20 or newer.
32
+ Node 20 or newer. `git` on the PATH for packages and `sync`. `claude` on the PATH if your shed lists plugins.
33
33
 
34
34
  ## Quick start
35
35
 
36
36
  ```bash
37
37
  # 1. On the machine that already has your setup
38
38
  lshed init --shed ~/lshed
39
- cd ~/lshed && git init && git add -A && git commit -m "my harness" && git remote add origin <your private repo> && git push -u origin main
39
+ cd ~/lshed && git init && git remote add origin <your private repo>
40
+ lshed sync # commit + push
40
41
 
41
42
  # 2. Edit ~/lshed/lshed.yaml — add profiles, drop parts you don't need everywhere
42
43
 
43
44
  # 3. On any other machine
44
45
  git clone <your private repo> ~/lshed
45
- lshed restore research --shed ~/lshed # --shed only needed the first time
46
+ lshed restore research --shed ~/lshed # --shed only needed the first time
46
47
  ```
47
48
 
49
+ ## How to use it
50
+
51
+ ### Day one: put what you have into a shed
52
+
53
+ ```
54
+ $ lshed init --shed ~/lshed --exclude _gstack-command connect-chrome
55
+ 스캔: /home/me/.claude → 창고: /home/me/lshed
56
+ ≡ package gstack github:garrytan/gstack@main @253d1df (참조만 기록)
57
+ ≡ package claude-plugins-official claude-marketplace:anthropics/claude-plugins-official (참조만 기록)
58
+ ≡ package exa claude-plugin:exa@claude-plugins-official @3.4.1 (참조만 기록)
59
+ · skills/browse (gstack 가 생성한 것 → 건너뜀)
60
+ · skills/review (gstack 가 생성한 것 → 건너뜀)
61
+ - skills/_gstack-command (--exclude)
62
+ + skills/add-drivers
63
+ + skills/domain-modeling
64
+ + mcp/notion (시크릿 → ${NOTION_AUTHORIZATION})
65
+ + instructions/main (CLAUDE.md)
66
+
67
+ lshed.yaml 생성: /home/me/lshed/lshed.yaml (부품 4개, 패키지 3개, 생성물 53개 건너뜀, 제외 2개, 프로필 "default")
68
+ ```
69
+
70
+ `init` reads your agent root and writes only to the shed and `~/.claude/lshed/`. It sorts everything into [three kinds](#three-kinds-of-things): authored parts are copied (`+`), things you installed become packages recorded by source and version (`≡`), and files an installer generated are skipped (`·`). Aliases an installer created without symlinks look authored; leave them out with `--exclude`, and lshed remembers that under `exclude:` in the manifest.
71
+
72
+ Then open `lshed.yaml`. It has one profile, `default`, listing everything. Fill in `install:` for git packages that need a post-clone step, and make it a git repo:
73
+
74
+ ```
75
+ cd ~/lshed && git init && git remote add origin git@github.com:me/harness.git
76
+ lshed sync
77
+ ```
78
+
79
+ ### Every day: edit, save, sync
80
+
81
+ You edit skills where the agent reads them, in `~/.claude`. The shed does not change by itself.
82
+
83
+ ```
84
+ lshed status # what profile is applied, what drifted, what is new
85
+ lshed diff # file-level differences between ~/.claude and the shed
86
+ lshed save # copy local edits into the shed (or: lshed save skills/add-drivers)
87
+ lshed sync # commit the shed, pull, push
88
+ ```
89
+
90
+ `save` is the only path from `~/.claude` to the shed, and it only works for parts the shed owns (`file:` sources). `sync` warns if you have unsaved edits so you do not push a shed that is behind your machine.
91
+
92
+ ### A new machine
93
+
94
+ ```
95
+ git clone git@github.com:me/harness.git ~/lshed
96
+ lshed restore default --shed ~/lshed
97
+ ```
98
+
99
+ ```
100
+ + package gstack (clone https://github.com/garrytan/gstack.git @main → 253d1df)
101
+ + package claude-plugins-official (claude plugin marketplace add anthropics/claude-plugins-official)
102
+ + package exa (claude plugin install exa@claude-plugins-official (전에 3.4.1; 고정은 안 됨))
103
+ + skills/add-drivers
104
+ + skills/domain-modeling
105
+ + mcp:notion (${NOTION_AUTHORIZATION})
106
+ + lshed/instructions/main.md
107
+ + CLAUDE.md
108
+
109
+ 프로필 "default" 적용: 배치 5, 제거 0, 패키지 설치 3
110
+
111
+ 설치 명령 1개를 실행하지 않았습니다. 확인 후 '--yes' 로 다시 실행하거나 직접 돌리세요:
112
+ cd /home/me/.claude/skills/gstack && ./setup
113
+
114
+ 환경변수가 없는 항목이 있습니다. 시크릿 값은 창고에 담지 않으므로 이 기기의 셸 환경에 넣으세요 (예: ~/.zshrc 의 export):
115
+ mcp:notion: NOTION_AUTHORIZATION
116
+ ```
117
+
118
+ Two things need you afterwards. Package `install:` commands are shell commands from a repository you cloned, so `restore` shows them and stops; run them yourself or rerun with `--yes`. MCP servers reference secrets as `${VAR}`; export the variables in your shell and Claude Code fills them in. From then on `lshed restore` with no arguments reapplies the last profile, and the shed location is remembered.
119
+
120
+ ### Profiles
121
+
122
+ A profile is a list of ids per category. Add as many as you like to `lshed.yaml`:
123
+
124
+ ```yaml
125
+ profiles:
126
+ default:
127
+ packages: [gstack, claude-plugins-official, exa]
128
+ skills: [add-drivers, domain-modeling, grilling]
129
+ instructions: [main]
130
+ mcp: [notion]
131
+ server: # headless box: no browser toolkit, no MCP
132
+ skills: [add-drivers]
133
+ instructions: [main, server-rules]
134
+ ```
135
+
136
+ ```
137
+ lshed restore server
138
+ - skills/domain-modeling
139
+ - skills/grilling
140
+ - mcp:notion
141
+ = skills/add-drivers
142
+ ~ CLAUDE.md
143
+ + lshed/instructions/server-rules.md
144
+ ```
145
+
146
+ Switching removes only what the previous profile placed (`-`), keeps what both use (`=`), and rewrites what changed (`~`). Everything removed or overwritten goes to `~/.claude/lshed/backups/<timestamp>/` first. Packages are additive: a profile that does not list `gstack` leaves the clone alone. `--dry-run` prints this plan without touching anything.
147
+
148
+ Instructions fragments are ordered. `restore` writes a `CLAUDE.md` that `@`-imports each fragment, so editing a fragment in the shed shows up on the next `restore` and there is nothing to merge.
149
+
150
+ ### Adding things later
151
+
152
+ Write a new skill, add an MCP server with `claude mcp add`, clone a toolkit into `~/.claude/skills/`. Then:
153
+
154
+ ```
155
+ $ lshed add
156
+ 창고에 없는 항목 3개 (넣으려면 lshed add <key...> 또는 --all):
157
+ skills/paper-review
158
+ mcp/linear
159
+ ≡ packages/superpowers github:obra/superpowers@main
160
+ · 패키지 gstack 가 생성한 것 53개는 담지 않습니다
161
+
162
+ $ lshed add paper-review mcp/linear
163
+ + skills/paper-review
164
+ + mcp/linear (시크릿 → ${LINEAR_API_KEY})
165
+
166
+ 2개를 창고에 넣고 프로필 "default" 에 추가했습니다. 창고를 커밋하세요: /home/me/lshed
167
+ ```
168
+
169
+ `add` classifies exactly like `init`, appends to `lshed.yaml` without disturbing your comments, adds the parts to the current profile and to the managed set. Without keys it only lists. `status` shows the count as `창고 밖`. To put a part that is already in the shed into another profile, edit `profiles:` by hand; `add` tells you when that is the case.
170
+
171
+ ### Keeping packages current
172
+
173
+ ```
174
+ lshed status # shows "253d1df ≠ lock 0d1bd56 → lshed update" when a clone moved
175
+ lshed update # fast-forward every package in the profile, refresh lshed.lock
176
+ lshed update gstack --yes # one package, and run its install: afterwards
177
+ ```
178
+
179
+ Git packages are pinned by commit in `lshed.lock`; a new machine gets exactly that commit. Plugins cannot be pinned, so the lock records what got installed and `status` says when it differs from the machine you came from.
180
+
181
+ ### Housekeeping
182
+
183
+ ```
184
+ lshed list # everything in the shed and which profiles use it
185
+ lshed list --unused # parts no profile lists
186
+ lshed remove skills/old # delete from the shed (refused while a profile uses it)
187
+ lshed prune --yes # delete everything unused
188
+ ```
189
+
190
+ `remove` and `prune` delete from the shed without a backup; the shed lives in git, so commit before you prune.
191
+
192
+ ### Reading the output
193
+
194
+ | Mark | Meaning |
195
+ |---|---|
196
+ | `+` | placed / added |
197
+ | `=` | already identical, nothing done |
198
+ | `~` | existed with different content, replaced (backed up) |
199
+ | `-` | removed (backed up) or excluded |
200
+ | `≡` | package: recorded by source, not copied |
201
+ | `·` | generated by an installer, skipped |
202
+ | `!` | needs your attention |
203
+ | `↑` `↓` | pushed / pulled (sync), updated (update) |
204
+
205
+ Errors go to stderr with exit code 1. Everything else is on stdout.
206
+
48
207
  ## The manifest
49
208
 
50
- `lshed.yaml` lives at the root of the shed. `init` generates it; edit it by hand from then on.
209
+ `lshed.yaml` lives at the root of the shed. `init` generates it; edit it by hand from then on. `add` and `remove` edit it for you and keep your comments.
51
210
 
52
211
  ```yaml
53
212
  version: 1
54
213
  agent: claude-code
214
+ exclude: [skills/_gstack-command] # things init/add must not pick up
215
+ ignore: [dist] # extra names never copied (adds to the built-in list)
55
216
 
56
217
  components:
57
218
  skills:
@@ -66,38 +227,34 @@ components:
66
227
  - id: research-style
67
228
  mcp:
68
229
  - id: exa # file:./mcp/exa.json — secrets replaced by ${VAR}
230
+ settings:
231
+ - id: permissions # file:./settings/permissions.json — one top-level key of settings.json
232
+ - id: hooks
233
+
234
+ packages:
235
+ - id: gstack
236
+ source: github:garrytan/gstack@main
237
+ into: skills/gstack
238
+ install: ./setup
69
239
 
70
240
  profiles:
71
241
  research:
242
+ packages: [gstack]
72
243
  skills: [paper-review]
73
244
  agents: [reviewer]
74
245
  instructions: [base, research-style] # order matters
75
246
  mcp: [exa]
247
+ settings: [permissions, hooks]
76
248
  teaching:
77
249
  skills: [grading-helper]
78
250
  commands: [summarize]
79
251
  instructions: [base]
80
252
  ```
81
253
 
82
- - Component `source` accepts `file:<path relative to the shed>`. Package `source` accepts `github:owner/repo@ref` or `git:<url>#ref`.
83
- - Category names come from the adapter. For Claude Code: `skills`, `agents`, `commands`, `instructions`, `mcp`.
84
- - `ignore:` at the top level adds to the built-in list of things never copied: `node_modules`, `.git`, `__pycache__`, `.venv`, cache directories, `*.log`. Build output like `dist/` is not ignored by default, since some skills ship it. Add it yourself if your parts rebuild from source.
85
- - Instructions are not merged. `restore` writes a `CLAUDE.md` that `@`-imports each fragment in order, so a fragment edit shows up without re-running anything. Your original `CLAUDE.md` is backed up the first time.
86
-
87
- ## MCP servers and secrets
88
-
89
- User-scope MCP servers live in `~/.claude.json`, next to machine IDs and session state. lshed treats each server as a component of category `mcp`: the shed holds `mcp/<name>.json`, and `restore` edits only the `mcpServers.<name>` key of `~/.claude.json`, leaving everything else in that file alone.
90
-
91
- **No secret value enters the shed.** `init` replaces values under `env` and `headers` whose key looks like a secret (`key`, `token`, `secret`, `pass`, `auth`, `credential`, `cookie`, `session`) with a `${VAR}` placeholder:
92
-
93
- ```json
94
- { "type": "stdio", "command": "npx", "args": ["-y", "exa-mcp-server"],
95
- "env": { "EXA_API_KEY": "${EXA_API_KEY}" } }
96
- { "type": "http", "url": "https://mcp.notion.com/mcp",
97
- "headers": { "Authorization": "Bearer ${NOTION_AUTHORIZATION}" } }
98
- ```
99
-
100
- `restore` writes the placeholder as is. Claude Code expands `${VAR}` from the environment when it starts the server, so the value only ever lives in your shell (`export EXA_API_KEY=...` in `~/.zshrc`, or however you manage secrets). `restore` and `status` list the variables the profile needs that are not set. The heuristic is a suggestion: edit the JSON in the shed to add or remove placeholders, and `init` warns when something in `args` or `url` looks like a token. `save` keeps existing placeholders and masks new secret-looking keys, so a rotated key never leaks into the shed by accident.
254
+ - Component `source` accepts `file:<path relative to the shed>`. Package `source` accepts `github:owner/repo@ref`, `git:<url>#ref`, `claude-marketplace:<owner/repo>`, `claude-plugin:<name>@<marketplace>`.
255
+ - Category names come from the adapter. For Claude Code: `skills`, `agents`, `commands`, `instructions`, `mcp`, `settings`.
256
+ - `ignore:` adds to the built-in list of things never copied: `node_modules`, `.git`, `__pycache__`, `.venv`, cache directories, `*.log`. Build output like `dist/` is not ignored by default, since some skills ship it.
257
+ - `exclude:` lists parts that exist locally but must not enter the shed. `init --exclude` writes it.
101
258
 
102
259
  ## Three kinds of things
103
260
 
@@ -105,77 +262,89 @@ A real `~/.claude` mixes three kinds of content, and they need different handlin
105
262
 
106
263
  | Kind | Example | What lshed does |
107
264
  |---|---|---|
108
- | **Authored** | a skill you wrote, your `CLAUDE.md` | copies it into the shed |
109
- | **Installed** | a toolkit you `git clone`d, a plugin | records source + commit; `restore` clones it back |
265
+ | **Authored** | a skill you wrote, your `CLAUDE.md`, an MCP server you added | copies it into the shed |
266
+ | **Installed** | a toolkit you `git clone`d, a plugin | records source + commit; `restore` clones or installs it back |
110
267
  | **Generated** | stub skills an installer wrote for you | skips them; they return when the installer runs |
111
268
 
112
- `init` sorts this out for you. A directory with a `.git` and a remote becomes a **package**. A skill whose symlink points inside a package is treated as generated and skipped. Everything else is authored and copied.
269
+ A directory with a `.git` and a remote becomes a **package**. A skill whose symlink points inside a package is treated as generated and skipped. Everything else is authored and copied. The rules that keep this safe:
113
270
 
114
- ```yaml
115
- packages:
116
- - id: gstack
117
- source: github:garrytan/gstack@main # git:<url>#ref for other hosts
118
- into: skills/gstack # where it lives under ~/.claude
119
- install: ./setup # optional; run after clone, only with --yes
271
+ - A package that is already present is never touched by `restore`. Your local checkout is yours.
272
+ - `install:` is a shell command. `restore` and `update` **print it and stop** unless you pass `--yes`. Plugin installs go through Claude Code's own package manager and run without it; `--yes` is forwarded as `-y` for plugins that declare an install command.
273
+ - Packages are not part of the managed set. Switching profiles never deletes a clone.
120
274
 
121
- profiles:
122
- default:
123
- packages: [gstack]
124
- skills: [add-drivers, domain-modeling]
125
- ```
275
+ Claude Code plugins are packages with their own scheme. `init` finds user-scope ones in `~/.claude/plugins`; `restore` adds the marketplace first, then runs `claude plugin install`. Project-scope plugins belong to their project and are not recorded.
126
276
 
127
- `lshed.lock` pins each package to a commit, so a fresh machine gets the same version you had. `lshed update` moves it forward.
277
+ ## MCP servers and secrets
128
278
 
129
- Claude Code plugins are packages too, with their own scheme. `init` finds them in `~/.claude/plugins`:
279
+ User-scope MCP servers live in `~/.claude.json`, next to machine IDs and session state. lshed treats each server as a component of category `mcp`: the shed holds `mcp/<name>.json`, and `restore` edits only the `mcpServers.<name>` key of `~/.claude.json`, leaving everything else in that file alone.
130
280
 
131
- ```yaml
132
- packages:
133
- - id: claude-plugins-official
134
- source: claude-marketplace:anthropics/claude-plugins-official
135
- - id: exa
136
- source: claude-plugin:exa@claude-plugins-official
281
+ **No secret value enters the shed.** `init` and `add` replace values under `env` and `headers` whose key contains a secret-looking word (`key`, `token`, `secret`, `password`, `auth`, `authorization`, `credential`, `cookie`, `session` — whole words, so `MAX_OUTPUT_TOKENS` is left alone) with a `${VAR}` placeholder:
282
+
283
+ ```json
284
+ { "type": "stdio", "command": "npx", "args": ["-y", "exa-mcp-server"],
285
+ "env": { "EXA_API_KEY": "${EXA_API_KEY}" } }
286
+ { "type": "http", "url": "https://mcp.notion.com/mcp",
287
+ "headers": { "Authorization": "Bearer ${NOTION_AUTHORIZATION}" } }
137
288
  ```
138
289
 
139
- `restore` adds the marketplace, then runs `claude plugin install exa@claude-plugins-official`. Plugins cannot be pinned to a version, so the lock records whatever got installed and `status` tells you when it differs from the machine you came from. Plugins that bundle MCP servers bring them along.
290
+ `restore` writes the placeholder as is. Claude Code expands `${VAR}` from the environment when it starts the server, so the value only ever lives in your shell (`export EXA_API_KEY=...` in `~/.zshrc`, or however you manage secrets). `restore` and `status` list the variables the profile needs that are not set. The heuristic is a suggestion: edit the JSON in the shed to add or remove placeholders, and `init` warns when something in `args` or `url` looks like a token. `save` keeps existing placeholders and masks new secret-looking keys, so a rotated key never leaks into the shed by accident. `diff` compares with placeholders as wildcards, so a machine holding real values is not drift.
140
291
 
141
- Rules that keep this safe:
292
+ ## Settings
142
293
 
143
- - A package that is already present is never touched by `restore`. Your local checkout is yours.
144
- - `install:` is a shell command. `restore` and `update` **print it and stop** unless you pass `--yes`. Plugin installs go through Claude Code's own package manager and run without it; `--yes` is forwarded as `-y` for plugins that declare an install command.
145
- - Packages are not part of the managed set. Switching profiles never deletes a clone.
146
- - Installers sometimes create aliases without symlinks, which `init` cannot tell from authored skills. Leave those out with `--exclude`:
294
+ `~/.claude/settings.json` holds hooks, permissions, `env`, the model, the theme, and some state Claude Code writes for itself. lshed does not merge it. Each **top-level key is one component** of category `settings`: the shed holds `settings/permissions.json`, `settings/hooks.json`, and so on, and `restore` writes exactly those keys, leaving the rest of the file alone. A profile can carry `permissions` and `hooks` and leave `model` to each machine.
147
295
 
148
- ```bash
149
- lshed init --shed ~/harness --exclude _gstack-command connect-chrome
150
296
  ```
297
+ $ lshed add
298
+ 창고에 없는 항목 3개:
299
+ settings/hooks ! 패키지 gstack 안을 가리킵니다. 그 설치가 만든 것이면 exclude 하세요: settings/hooks
300
+ settings/model
301
+ settings/theme
302
+ ```
303
+
304
+ - `enabledPlugins` is never taken: the plugin packages own it, and `restore` rebuilds it by installing them.
305
+ - Absolute paths under your home directory become `${HOME}/…` in the shed, so a hook command written on one machine works on another. Claude Code does not expand variables in `settings.json`, so `restore` fills `${HOME}` and any `${VAR}` itself from your shell; unset variables are reported and left as placeholders.
306
+ - `env` is treated as a secret map: keys that look secret are masked, the rest (`CLAUDE_CODE_MAX_OUTPUT_TOKENS`, …) travel as they are.
307
+ - A value pointing inside a package (a hook a toolkit's installer wrote) is flagged. If the installer recreates it, put it in `exclude:` and let `restore --yes` bring it back.
308
+ - Since the shed owns the whole key, extra permissions you grant locally show up in `diff` and go into the shed with `save`, like any other edit.
151
309
 
152
310
  ## Commands
153
311
 
154
312
  ```
155
313
  lshed init [--shed <dir>] [--profile <name>] [--exclude <id...>]
314
+ lshed add [keys...] [--all] put things that appeared since init into the shed
156
315
  lshed restore [profile] [--dry-run] [--no-backup] [--yes]
157
- lshed update [ids...] [--dry-run] [--yes] pull packages forward, refresh lshed.lock
158
- lshed status applied profile, managed paths, drift, packages
159
- lshed diff files that differ between local and shed
316
+ lshed status applied profile, drift, packages, missing env, new things
317
+ lshed diff files (or JSON keys) that differ between local and shed
160
318
  lshed save [ids...] copy local edits back into the shed
319
+ lshed sync [-m <msg>] [--no-push] [--dry-run] commit the shed, pull --rebase, push
320
+ lshed update [ids...] [--dry-run] [--yes] pull packages forward, refresh lshed.lock
161
321
  lshed list [--unused] what is in the shed, and which profiles use it
162
322
  lshed remove <key> drop a component or package from the shed
163
323
  lshed prune [--yes] drop everything no profile uses
164
324
  ```
165
325
 
166
- `remove` and `prune` delete from the shed without a backup. The shed is meant to live in git; commit before you prune. Both refuse to touch anything a profile still lists, so the way to retire a part is to take it out of the profiles first.
326
+ Keys are `category/id`, or just `id` when unambiguous: `skills/paper-review`, `mcp/exa`, `packages/gstack`.
167
327
 
168
- Global options: `--shed <dir>` (or `LSHED_HOME`; after the first restore lshed remembers it), `--root <dir>` (agent config root, default `~/.claude`).
328
+ Global options: `--shed <dir>` (or `LSHED_HOME`; after the first restore lshed remembers it), `--root <dir>` (agent config root, default `~/.claude` or `CLAUDE_CONFIG_DIR`).
169
329
 
170
330
  ### What `restore` does
171
331
 
172
- 0. Clones any package in the profile that is missing, at the commit in `lshed.lock`.
332
+ 0. Installs any package in the profile that is missing, at the version in `lshed.lock`.
173
333
  1. Removes paths that the **previous** profile placed and the new one doesn't need.
174
- 2. Copies every part of the new profile into place.
334
+ 2. Copies every part of the new profile into place; writes MCP entries into `~/.claude.json` and settings keys into `settings.json`.
175
335
  3. Regenerates the instructions file.
176
336
 
177
337
  Anything it overwrites or removes is backed up first under `~/.claude/lshed/backups/<timestamp>/`, unless you pass `--no-backup`. Files lshed never placed are left alone. `--dry-run` prints the plan and writes nothing.
178
338
 
339
+ ### What `sync` does
340
+
341
+ 1. Warns if `diff` shows local edits you have not saved.
342
+ 2. Commits everything in the shed (message names the changed parts, or `-m`).
343
+ 3. If `origin` exists: `git pull --rebase`, then `git push` (sets the upstream the first time).
344
+ 4. If commits came in, tells you to run `lshed restore`.
345
+
346
+ On a conflict it aborts the rebase, leaves the shed clean with your commit intact, and tells you to resolve with git. Without a remote it only commits. It never runs `save` for you.
347
+
179
348
  ### Ownership
180
349
 
181
350
  The shed is the source of truth for authored parts: `save` copies local edits back for `file:` components. Packages are owned by their upstream: `update` pulls them, `save` ignores them.
@@ -184,16 +353,19 @@ The shed is the source of truth for authored parts: `save` copies local edits ba
184
353
 
185
354
  ```
186
355
  <shed>/
187
- lshed.yaml
188
- skills/<id>/ agents/<id>.md commands/<id>.md instructions/<id>.md
189
- mcp/<id>.json ← secrets as ${VAR}
356
+ lshed.yaml manifest
357
+ lshed.lock package versions (generated)
358
+ skills/<id>/ agents/<id>.md commands/<id>.md instructions/<id>.md
359
+ mcp/<id>.json secrets as ${VAR}
360
+ settings/<id>.json one top-level key each; home paths as ${HOME}
190
361
 
191
362
  ~/.claude/
192
363
  skills/ agents/ commands/ CLAUDE.md ← placed by restore
193
- ~/.claude.json mcpServers.<id> ← one key per mcp component; the rest of the file is untouched
364
+ settings.json <id> ← one key per settings component; the rest is untouched
194
365
  lshed/state.json ← which profile, which paths are managed
195
366
  lshed/instructions/<id>.md ← fragments imported by CLAUDE.md
196
367
  lshed/backups/<timestamp>/ ← whatever restore replaced
368
+ ~/.claude.json mcpServers.<id> ← one key per mcp component; the rest of the file is untouched
197
369
  ```
198
370
 
199
371
  `state.json` is per machine and is not part of the shed. If `CLAUDE_CONFIG_DIR` is set, lshed uses it as the root and expects `.claude.json` inside it, as Claude Code does.
@@ -201,16 +373,19 @@ The shed is the source of truth for authored parts: `save` copies local edits ba
201
373
  ## Not in scope (yet)
202
374
 
203
375
  - Secrets beyond "name the variable". Encrypted values, `op://` references and OS keychains are possible later; today lshed is deliberately no better than dotfiles here.
204
- - Project-scope MCP servers (`.mcp.json`, `~/.claude.json` `projects.*`). They belong to the project.
205
- - `settings.json` merging (hooks, permissions).
206
- - `sync` (a git pull/push wrapper). Use git in the shed directly for now.
207
- - Windows and macOS have not been tested. The code avoids platform-specific paths, but treat 0.1 as Linux/WSL.
376
+ - Project-scope MCP servers (`.mcp.json`, `~/.claude.json` `projects.*`) and project-scope plugins. They belong to the project.
377
+ - Windows and macOS have not been tested. The code avoids platform-specific paths, but treat this as Linux/WSL for now.
208
378
 
209
379
  ## Troubleshooting
210
380
 
211
381
  - **"창고 위치를 모릅니다"** — pass `--shed <dir>` or set `LSHED_HOME`. After one successful `restore`, lshed remembers it.
212
382
  - **restore replaced my `CLAUDE.md`** — it is in `~/.claude/lshed/backups/<timestamp>/CLAUDE.md`. Move its content into a fragment in the shed and add that fragment to your profile.
213
- - **I edited a skill locally and want to keep it** — `lshed diff` to see, `lshed save <id>` to push it into the shed, then commit the shed.
383
+ - **I edited a skill locally and want to keep it** — `lshed diff` to see, `lshed save <id>` to push it into the shed, then `lshed sync`.
384
+ - **`status` says a package differs from the lock** — something updated the clone or plugin behind lshed's back (Claude Code auto-updates plugins). `lshed update` records the new version.
385
+ - **`status` keeps listing the same new things** — they are installer aliases or scratch. Add them to `exclude:` in `lshed.yaml`.
386
+ - **restore says an MCP variable is missing** — export it in your shell profile and restart Claude Code. The placeholder in `~/.claude.json` is correct; Claude Code fills it at startup.
387
+ - **restore wrote a hook with the wrong path** — the shed stores home paths as `${HOME}/…`. If a command points elsewhere on this machine, edit the JSON in the shed to use `${HOME}` or another variable and `restore` again.
388
+ - **sync stopped on a conflict** — `cd <shed> && git pull --rebase`, resolve, `git rebase --continue`, then `lshed sync` again.
214
389
 
215
390
  ## License
216
391