Skip to content

Commit 462f652

Browse files
author
sfi-hub
committed
Initial release
0 parents  commit 462f652

11 files changed

Lines changed: 477 additions & 0 deletions

File tree

.gitattributes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
* text=auto eol=lf
2+
*.sh text eol=lf

.github/workflows/ci.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
name: ci
2+
on: [push, pull_request]
3+
permissions:
4+
contents: read
5+
jobs:
6+
test:
7+
strategy:
8+
matrix:
9+
os: [ubuntu-latest, macos-latest]
10+
runs-on: ${{ matrix.os }}
11+
steps:
12+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
13+
- name: shellcheck
14+
if: runner.os == 'Linux'
15+
run: sudo apt-get update -q && sudo apt-get install -y -q shellcheck && shellcheck -s sh install.sh uninstall.sh tests/test.sh
16+
- name: tests
17+
run: sh tests/test.sh

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
*.bak.*
2+
*.tmp.*
3+
.DS_Store
4+
PUBLISHING/*.zip
5+
_LOCAL/

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Changelog
2+
3+
## 1.0.0 - 2026-08-27
4+
First release.

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 sfi-hub
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# claude-remote-control-off
2+
3+
Turns off Claude Code's "Remote Control" feature and keeps it off.
4+
5+
```sh
6+
curl -fsSL https://raw.githubusercontent.com/sfi-hub/claude-remote-control-off/main/install.sh | sh
7+
```
8+
9+
Restart Claude Code afterwards. That's the whole thing.
10+
11+
![what the script prints when you run it with --dry-run](assets/demo.gif)
12+
13+
## What you see today, and what you get
14+
15+
**Today.** Every time you open Claude Code, a blue bar sits above your prompt:
16+
17+
```
18+
+------------------------------------------------------------------+
19+
| Remote Control is active . Continue here, on your phone, x |
20+
| or at claude.ai/code |
21+
+------------------------------------------------------------------+
22+
> _
23+
```
24+
25+
You click the **x**. It comes back next time. That's because the x only hides the message; the feature underneath stays on.
26+
27+
**After running the one line above.** You open Claude Code:
28+
29+
```
30+
> _
31+
```
32+
33+
That's it. No bar, no phone link, nothing to click away. And it stays that way, because the feature itself is now off, not just the message.
34+
35+
**How it works, in three steps**
36+
37+
```
38+
1. You paste one line 2. It writes two settings 3. You restart Claude Code
39+
into your terminal into your own settings file, and the bar is gone -
40+
(no password asked) after making a backup every session, every project
41+
```
42+
43+
## Why I wrote this
44+
45+
One morning Claude Code greeted me with a blue bar: *"Remote Control is active · Continue here, on your phone, or at claude.ai/code"*. I hadn't turned anything on. I clicked the ×, and it was back the next day.
46+
47+
It turns out the × only hides the message. The feature underneath - the one that lets claude.ai and the phone app drive the session on your machine - stays on, and there's a server-side flag that can switch it on again ([#89752](https://github.com/anthropics/claude-code/issues/89752)).
48+
49+
Anthropic does document a real off switch, it's just two lines in a JSON file most people have never opened. So I wrote a script that adds those two lines properly - backup first, check the result, don't touch anything else - and I've been using it since. Sharing it in case you'd like your prompt back too.
50+
51+
## What it actually does
52+
53+
It edits your personal Claude Code settings, `~/.claude/settings.json`, and sets:
54+
55+
```json
56+
"disableRemoteControl": true,
57+
"remoteControlAtStartup": false
58+
```
59+
60+
The first one is the off switch ([docs](https://code.claude.com/docs/en/settings-reference#disableremotecontrol)). The second one stops the auto-connect at launch and is the setting the VS Code toggle flips, so I pin it too ([docs](https://code.claude.com/docs/en/settings-reference#remotecontrolatstartup)).
61+
62+
Before it writes anything it copies your file to `settings.json.bak.<date>` in the same folder. It then builds the new file, re-reads it to make sure the two keys are really there, and only then swaps it in. If your settings file isn't valid JSON it stops and tells you, rather than guessing.
63+
64+
No `sudo`, no password, no calls to anything - the only download is the `curl` you typed. It needs `python3`, `node` or `jq` on your machine to edit JSON safely; nearly everyone has one of those.
65+
66+
If you'd rather look before you leap:
67+
68+
```sh
69+
curl -fsSL -o install.sh https://raw.githubusercontent.com/sfi-hub/claude-remote-control-off/main/install.sh
70+
less install.sh # ~200 lines, plain shell
71+
sh install.sh --dry-run # shows the change, writes nothing
72+
sh install.sh
73+
```
74+
75+
## Changed your mind?
76+
77+
```sh
78+
sh install.sh --uninstall
79+
```
80+
81+
removes the two keys again. Or just copy the `.bak` file back over `settings.json`. Restart Claude Code either way.
82+
83+
## Things worth knowing
84+
85+
- **This turns the feature off; it does not just hide the bar.** If you like Remote Control and only want the bar gone, there's no setting for that yet - see [#66343](https://github.com/anthropics/claude-code/issues/66343) and [#86110](https://github.com/anthropics/claude-code/issues/86110).
86+
- On a Team or Enterprise plan, your admin's managed settings win over yours. Same two keys work there.
87+
- Windows: works in Git Bash and WSL. In plain PowerShell, open `%USERPROFILE%\.claude\settings.json` and add the two lines by hand.
88+
- To check it took: `sh install.sh --status`, or type `/remote-control` inside Claude Code.
89+
90+
## Contributing
91+
92+
Tests run against a throwaway config dir, never your real one: `sh tests/test.sh`. CI runs them on macOS and Ubuntu with shellcheck. Bug reports welcome, especially from Linux distros and shells I don't have.
93+
94+
MIT. Not affiliated with Anthropic.

SECURITY.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Security
2+
3+
This is a ~200-line POSIX shell script that edits two keys in one JSON file. Its whole threat surface is that you are running a script from the internet, so:
4+
5+
**What it will never do**
6+
- run `sudo` or ask for a password
7+
- make any network request (the only download is the one *you* make with `curl`)
8+
- touch any file other than `~/.claude/settings.json` (or `$CLAUDE_CONFIG_DIR/settings.json`) and a backup beside it
9+
- modify or delete any key other than `disableRemoteControl` and `remoteControlAtStartup`
10+
- write a file it cannot parse back (write to temp -> verify -> rename)
11+
12+
**How to check before running**
13+
```sh
14+
curl -fsSL -o install.sh https://raw.githubusercontent.com/sfi-hub/claude-remote-control-off/main/install.sh
15+
less install.sh # read it - it is short on purpose
16+
sh install.sh --dry-run # shows the change, writes nothing
17+
sh install.sh
18+
```
19+
Pin to a release tag instead of `main` if you want a version that cannot change under you.
20+
21+
**Reporting**
22+
Open a GitHub issue, or a private security advisory on this repository. No bounty, fast answers.

assets/demo.gif

96.2 KB
Loading

install.sh

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
#!/bin/sh
2+
# claude-remote-control-off - turn Claude Code "Remote Control" off, for good.
3+
#
4+
# What it does (and nothing else):
5+
# 1. Locates your Claude Code user settings file (~/.claude/settings.json,
6+
# or $CLAUDE_CONFIG_DIR/settings.json if that variable is set).
7+
# 2. Makes a timestamped backup next to it.
8+
# 3. Sets two documented keys:
9+
# "disableRemoteControl": true -> the kill-switch (feature off entirely)
10+
# "remoteControlAtStartup": false -> no auto-connect at startup
11+
# 4. Re-reads the result and proves the keys are there BEFORE replacing the file.
12+
#
13+
# It never uses sudo, never phones home, never touches any other key,
14+
# and never writes a file it cannot parse back. Safe to run twice.
15+
# Close Claude Code first: the setting is read at launch, and a running
16+
# instance could overwrite the file at the same moment.
17+
#
18+
# Docs for the keys:
19+
# https://code.claude.com/docs/en/settings-reference#disableremotecontrol
20+
# https://code.claude.com/docs/en/settings-reference#remotecontrolatstartup
21+
#
22+
# The whole script is a function called on the LAST line, so a download that
23+
# is cut short can never run a half-script.
24+
25+
set -eu
26+
umask 077
27+
28+
usage() {
29+
cat <<'TXT'
30+
usage: sh install.sh [option]
31+
(none) apply: disableRemoteControl=true, remoteControlAtStartup=false
32+
--dry-run show what would change, write nothing
33+
--status show current state, write nothing
34+
--uninstall remove the two keys (Remote Control available again)
35+
-h, --help this text
36+
settings file: $CLAUDE_CONFIG_DIR/settings.json, default ~/.claude/settings.json
37+
TXT
38+
}
39+
40+
say() { printf '%s\n' "$*"; }
41+
die() { printf 'error: %s\n' "$*" >&2; exit 1; }
42+
43+
# read_state FILE -> "<disableRemoteControl> <remoteControlAtStartup>", each true|false|unset, or "INVALID INVALID"
44+
read_state() {
45+
f="$1"
46+
[ -f "$f" ] || { say "unset unset"; return; }
47+
case "$JSON_TOOL" in
48+
python3) python3 - "$f" <<'PY'
49+
import json,sys
50+
try:
51+
d=json.load(open(sys.argv[1],encoding="utf-8"))
52+
if not isinstance(d,dict): raise ValueError
53+
except Exception:
54+
print("INVALID INVALID"); sys.exit(0)
55+
g=lambda k: json.dumps(d[k],separators=(",",":")) if k in d else "unset"
56+
print(g("disableRemoteControl"), g("remoteControlAtStartup"))
57+
PY
58+
;;
59+
node) node -e '
60+
const fs=require("fs");let d;
61+
try{d=JSON.parse(fs.readFileSync(process.argv[1],"utf8"));if(typeof d!=="object"||d===null||Array.isArray(d))throw 0}
62+
catch(e){console.log("INVALID INVALID");process.exit(0)}
63+
const g=k=>k in d?JSON.stringify(d[k]):"unset";
64+
console.log(g("disableRemoteControl"),g("remoteControlAtStartup"))' "$f" ;;
65+
jq) [ -s "$f" ] || { say "INVALID INVALID"; return; }
66+
jq -r 'if type!="object" then error("x") else . end
67+
| def g(k): if has(k) then (.[k]|tojson) else "unset" end;
68+
"\(g("disableRemoteControl")) \(g("remoteControlAtStartup"))"' "$f" 2>/dev/null || say "INVALID INVALID" ;;
69+
esac
70+
}
71+
72+
# write_state SRC DST MODE (apply|uninstall): merge SRC (or {} if absent) into DST
73+
write_state() {
74+
src="$1"; dst="$2"; m="$3"
75+
case "$JSON_TOOL" in
76+
python3) python3 - "$src" "$dst" "$m" <<'PY'
77+
import json,sys,os
78+
src,dst,mode=sys.argv[1:4]
79+
d=json.load(open(src,encoding="utf-8")) if os.path.exists(src) else {}
80+
if not isinstance(d,dict): raise SystemExit("settings.json top level is not an object")
81+
if mode=="apply":
82+
d["disableRemoteControl"]=True; d["remoteControlAtStartup"]=False
83+
else:
84+
d.pop("disableRemoteControl",None); d.pop("remoteControlAtStartup",None)
85+
with open(dst,"w",encoding="utf-8",newline="\n") as fh: json.dump(d,fh,indent=2,ensure_ascii=False); fh.write("\n")
86+
PY
87+
;;
88+
node) node -e '
89+
const fs=require("fs");const [src,dst,mode]=process.argv.slice(1);
90+
let d=fs.existsSync(src)?JSON.parse(fs.readFileSync(src,"utf8")):{};
91+
if(typeof d!=="object"||Array.isArray(d)||d===null) throw new Error("settings.json top level is not an object");
92+
if(mode==="apply"){d.disableRemoteControl=true;d.remoteControlAtStartup=false}
93+
else{delete d.disableRemoteControl;delete d.remoteControlAtStartup}
94+
fs.writeFileSync(dst,JSON.stringify(d,null,2)+"\n")' "$src" "$dst" "$m" ;;
95+
jq) if [ -f "$src" ]; then in="$src"; else in=/dev/null; fi
96+
if [ "$m" = apply ]; then
97+
jq --null-input --rawfile _ "$in" '(if $_=="" then {} else ($_|fromjson) end) | if type!="object" then error("top level is not an object") else . end | . + {disableRemoteControl:true, remoteControlAtStartup:false}' > "$dst"
98+
else
99+
jq --null-input --rawfile _ "$in" '(if $_=="" then {} else ($_|fromjson) end) | if type!="object" then error("top level is not an object") else . end | del(.disableRemoteControl, .remoteControlAtStartup)' > "$dst"
100+
fi ;;
101+
esac
102+
}
103+
104+
main() {
105+
MODE=apply
106+
case "${1:-}" in
107+
"") ;;
108+
--dry-run) MODE=dry ;;
109+
--status) MODE=status ;;
110+
--uninstall) MODE=uninstall ;;
111+
-h|--help) usage; exit 0 ;;
112+
*) printf 'unknown option: %s\n' "$1" >&2; usage >&2; exit 2 ;;
113+
esac
114+
115+
CONF_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
116+
case "$CONF_DIR" in -*) die "CLAUDE_CONFIG_DIR must not start with '-'" ;; esac
117+
SETTINGS="$CONF_DIR/settings.json"
118+
119+
JSON_TOOL=""
120+
# probe, don't just locate: a Windows Store "python3" alias exists on PATH but exits 9009
121+
if python3 -c 'pass' >/dev/null 2>&1; then JSON_TOOL=python3
122+
elif node -e '' >/dev/null 2>&1; then JSON_TOOL=node
123+
elif jq -n '.' >/dev/null 2>&1; then JSON_TOOL=jq
124+
fi
125+
126+
say "claude-remote-control-off"
127+
say " settings file : $SETTINGS"
128+
say " json tool : ${JSON_TOOL:-none}"
129+
[ -n "$JSON_TOOL" ] || die "need python3, node or jq to edit JSON safely (none found)"
130+
131+
if [ -L "$SETTINGS" ]; then
132+
die "$SETTINGS is a symlink (dotfile manager?) - point CLAUDE_CONFIG_DIR at the real directory, or edit the target yourself; nothing was changed"
133+
fi
134+
if [ -e "$SETTINGS" ] && [ ! -f "$SETTINGS" ]; then
135+
die "$SETTINGS exists but is not a regular file; nothing was changed"
136+
fi
137+
138+
if [ -f "$SETTINGS" ]; then
139+
st=$(read_state "$SETTINGS") || die "could not read $SETTINGS with $JSON_TOOL"
140+
case "$st" in INVALID*) die "$SETTINGS is not a valid JSON object - fix it (or move it aside) first; nothing was changed" ;; esac
141+
else
142+
st="unset unset"
143+
say " (no settings file yet - one will be created)"
144+
fi
145+
case "$st" in *" "*) ;; *) die "unexpected reader output: '$st'" ;; esac
146+
cur_disable=${st%% *}; cur_startup=${st##* }
147+
say " current : disableRemoteControl=$cur_disable remoteControlAtStartup=$cur_startup"
148+
149+
case "$MODE" in
150+
status) exit 0 ;;
151+
dry)
152+
say " would set : disableRemoteControl=true remoteControlAtStartup=false"
153+
say " (dry run - nothing written)"; exit 0 ;;
154+
apply)
155+
if [ "$cur_disable" = true ] && [ "$cur_startup" = false ]; then say " already off - nothing to do."; exit 0; fi ;;
156+
uninstall)
157+
if [ "$cur_disable" = unset ] && [ "$cur_startup" = unset ]; then say " keys not present - nothing to do."; exit 0; fi ;;
158+
esac
159+
160+
mkdir -p -- "$CONF_DIR"
161+
[ -w "$CONF_DIR" ] || die "$CONF_DIR is not writable"
162+
163+
# temp file in the SAME directory (atomic rename), unpredictable name, mode 600 (umask 077)
164+
tmp=$(mktemp -- "$SETTINGS.tmp.XXXXXX") || die "cannot create temp file in $CONF_DIR"
165+
trap 'rm -f -- "$tmp"' EXIT INT TERM
166+
167+
write_state "$SETTINGS" "$tmp" "$MODE" || die "could not build new settings - nothing changed"
168+
169+
# verify the CANDIDATE before it replaces anything
170+
new=$(read_state "$tmp") || die "could not re-read candidate - nothing changed"
171+
case "$MODE" in
172+
apply) want="true false" ;;
173+
uninstall) want="unset unset" ;;
174+
esac
175+
[ "$new" = "$want" ] || die "verification failed (got: $new) - nothing changed"
176+
177+
if [ -f "$SETTINGS" ]; then
178+
bak="$SETTINGS.bak.$(date +%Y%m%d-%H%M%S).$$"
179+
[ -e "$bak" ] && die "backup $bak already exists - nothing changed"
180+
cp -- "$SETTINGS" "$bak"
181+
chmod 600 "$bak" 2>/dev/null || true
182+
say " backup : $bak"
183+
fi
184+
185+
mv -- "$tmp" "$SETTINGS"
186+
trap - EXIT INT TERM
187+
chmod 600 "$SETTINGS" 2>/dev/null || true
188+
189+
st=$(read_state "$SETTINGS") || die "post-write read failed - restore from the backup above"
190+
[ "$st" = "$want" ] || die "post-write check failed (got: $st) - restore from the backup above"
191+
say " verified : disableRemoteControl=${st%% *} remoteControlAtStartup=${st##* }"
192+
say ""
193+
if [ "$MODE" = apply ]; then
194+
say "Done. Restart Claude Code (terminal, VS Code, desktop) - the setting is read at launch."
195+
say "Undo any time: sh install.sh --uninstall"
196+
else
197+
say "Done. Remote Control is available again after you restart Claude Code."
198+
fi
199+
}
200+
201+
main "$@"

0 commit comments

Comments
 (0)