@@ -2,7 +2,6 @@ package claude
22
33import (
44 "encoding/json"
5- "fmt"
65 "os"
76 "path/filepath"
87)
@@ -19,85 +18,34 @@ func ClaudeJSONPath() (string, error) {
1918}
2019
2120// EnsureRemoteControlAtStartup sets "remoteControlAtStartup": true in
22- // ~/.claude.json so Claude Code's Remote Control feature is on by default for
23- // any new Claude session — including those spawned by ctm on a freshly-
21+ // ~/.claude.json so Claude Code's Remote Control feature is on by default
22+ // for any new Claude session — including those spawned by ctm on a freshly-
2423// bootstrapped machine.
2524//
26- // Semantics (strictly conservative — the file is Claude Code's state, not ours):
27- // - If the file does not exist, do nothing. Never create it; Claude Code
28- // owns the lifecycle of ~/.claude.json .
29- // - If the key is already present (true OR false), do nothing. This
30- // respects an explicit user choice made via `/config`, including
31- // opt-out. "missing" and "false" are deliberately treated differently .
25+ // Semantics (strictly conservative — the file is Claude Code's state, not
26+ // ours):
27+ // - If the file does not exist, do nothing. Never create it .
28+ // - If the key is already present (true, false, or JSON null) we treat
29+ // that as a deliberate user choice and leave it alone. Only an absent
30+ // key triggers the write .
3231// - Only when the key is absent do we write it true, preserving all other
33- // keys byte-for-byte via json.RawMessage round-trip.
34- // - Writes are atomic (temp file + rename) and preserve the original file
35- // mode (typically 0600).
32+ // keys via json.RawMessage round-trip (values byte-exact; top-level
33+ // key order becomes alphabetical — see patchJSONFile).
3634//
3735// The key `remoteControlAtStartup` was discovered empirically by toggling
3836// "Enable Remote Control for all sessions" in `/config` and diffing the
3937// resulting JSON. It is not a documented/stable API; if Claude Code renames
40- // it, future runs of this function silently no-op (the old key would be
41- // absent, we'd add our own, Claude Code would ignore it — harmless).
38+ // it, future runs silently no-op (harmless).
4239//
43- // Concurrency caveat: if Claude Code writes to ~/.claude.json between our
44- // Read and our Rename, that write is lost. In practice ctm bootstrap runs
45- // before `claude` launches, so the race window is small and the impact
46- // (losing one Claude Code-authored update) is acceptable for this best-
47- // effort convenience feature. No flock to keep the bootstrap fast.
48- //
49- // Errors are returned so the caller can log; callers in ctm's boot path
50- // should swallow — remote-control defaults are convenience, not correctness,
51- // and must never block claude launch.
40+ // Errors are returned so callers can log; callers in ctm's boot path should
41+ // swallow — remote-control defaults are convenience, not correctness, and
42+ // must never block claude launch.
5243func EnsureRemoteControlAtStartup (path string ) error {
53- info , err := os .Stat (path )
54- if err != nil {
55- return nil
56- }
57-
58- data , err := os .ReadFile (path )
59- if err != nil {
60- return fmt .Errorf ("reading %s: %w" , path , err )
61- }
62-
63- var obj map [string ]json.RawMessage
64- if err := json .Unmarshal (data , & obj ); err != nil {
65- return fmt .Errorf ("parsing %s: %w" , path , err )
66- }
67-
68- if _ , present := obj ["remoteControlAtStartup" ]; present {
69- return nil
70- }
71-
72- obj ["remoteControlAtStartup" ] = json .RawMessage ("true" )
73-
74- out , err := json .MarshalIndent (obj , "" , " " )
75- if err != nil {
76- return fmt .Errorf ("marshalling %s: %w" , path , err )
77- }
78-
79- dir := filepath .Dir (path )
80- tmp , err := os .CreateTemp (dir , ".claude.json.*" )
81- if err != nil {
82- return fmt .Errorf ("creating temp file: %w" , err )
83- }
84- tmpPath := tmp .Name ()
85- // best-effort cleanup if we bail before rename
86- defer os .Remove (tmpPath ) //nolint:errcheck
87-
88- if _ , err := tmp .Write (out ); err != nil {
89- tmp .Close () //nolint:errcheck
90- return fmt .Errorf ("writing temp file: %w" , err )
91- }
92- if err := tmp .Chmod (info .Mode ().Perm ()); err != nil {
93- tmp .Close () //nolint:errcheck
94- return fmt .Errorf ("chmod temp file: %w" , err )
95- }
96- if err := tmp .Close (); err != nil {
97- return fmt .Errorf ("closing temp file: %w" , err )
98- }
99- if err := os .Rename (tmpPath , path ); err != nil {
100- return fmt .Errorf ("renaming temp file: %w" , err )
101- }
102- return nil
44+ return patchJSONFile (path , func (obj map [string ]json.RawMessage ) bool {
45+ if _ , present := obj ["remoteControlAtStartup" ]; present {
46+ return false
47+ }
48+ obj ["remoteControlAtStartup" ] = json .RawMessage ("true" )
49+ return true
50+ })
10351}
0 commit comments