Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 69 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Custom wrapper for Claude Code CLI with identity management and 1Password secret

## Features

- **Auto Remote Control**: Every interactive session is automatically named and accessible from claude.ai/code and the Claude mobile app
- **Git Identity Management**: Separate git identity for Claude Code operations
- **SSH Key Isolation**: Dedicated SSH key for Claude git operations
- **GitHub Token Management**: Separate GitHub CLI token
Expand Down Expand Up @@ -126,7 +127,7 @@ EOF
Use `claude` command as normal:

```bash
# Normal usage
# Normal usage — automatically starts a named remote session
claude

# With debug output
Expand All @@ -135,14 +136,49 @@ CLAUDE_DEBUG=true claude
# Pass arguments
claude -c "your command here"
claude --version

# Opt out of remote control for one session
CLAUDE_NO_REMOTE_CONTROL=true claude

# Already has --remote-control? Wrapper skips injection
claude --remote-control "Custom Name"
```

The wrapper:

1. Sets git identity
2. Authenticates with 1Password (once per session)
3. Loads secrets from multi-level files
4. Passes through to real Claude CLI
4. Injects `--remote-control <session-name>` for interactive sessions
5. Passes through to real Claude CLI

### Remote Control

Every interactive `claude` invocation automatically registers a remote session named after the current git repository (or directory when outside a git repo). This lets you pick up any session from [claude.ai/code](https://claude.ai/code) or the Claude mobile app.

```
~/Developer/my-app $ claude
# → equivalent to: claude --remote-control "my-app"
```

Session naming priority (per Claude docs):

1. Name injected by the wrapper (repo or directory name)
2. `/rename` inside the session
3. Last meaningful message in conversation history
4. Your first prompt

**Opt-out options:**

| Method | Scope |
| ------ | ----- |
| `CLAUDE_NO_REMOTE_CONTROL=true claude` | Single session |
| `export CLAUDE_NO_REMOTE_CONTROL=true` | Shell session |
| `/config` → "Enable Remote Control for all sessions" → `false` | Persisted in Claude config |

Remote control is skipped automatically for non-interactive invocations: `--print`/`-p`, `--version`, `--help`, subcommands (`remote-control`, `mcp`, etc.), and script automation (`--no-session-persistence`).

> **Requirements**: Claude Code v2.1.51+, claude.ai authentication (not API key), Pro/Max/Team/Enterprise plan.

## Documentation

Expand Down Expand Up @@ -170,6 +206,7 @@ The wrapper:
```bash
# Run all automated tests
./tests/test-wrapper.sh
./tests/test-remote-session.sh

# Run with verbose output
VERBOSE=true ./tests/test-wrapper.sh
Expand All @@ -190,12 +227,14 @@ claude-wrapper/
│ ├── gh-token-router.sh # Per-invocation token selection (sourced by gh wrapper)
│ ├── secrets-loader.sh # 1Password integration
│ ├── binary-discovery.sh # Claude binary search/validation
│ └── pre-launch.sh # Project-specific pre-launch hooks
│ ├── pre-launch.sh # Project-specific pre-launch hooks
│ └── remote-session.sh # Automatic remote control session naming
├── docs/
│ ├── SECRETS.md # 1Password documentation
│ └── SECURITY.md # Security hardening documentation
├── tests/
│ ├── test-wrapper.sh # Test suite
│ ├── test-remote-session.sh # Remote session module tests
│ └── README.md # Test documentation
├── .claude/ # Project-specific Claude config
├── .gitignore
Expand Down Expand Up @@ -249,6 +288,24 @@ claude --agent code-reviewer bin/claude-wrapper lib/

## Troubleshooting

### Remote control not connecting

```bash
# Verify Claude Code version (requires v2.1.51+)
claude --version

# Check you're using claude.ai auth (not API key)
# API keys do not support Remote Control

# Disable and re-enable for one session
CLAUDE_NO_REMOTE_CONTROL=true claude

# Debug injection
CLAUDE_DEBUG=true claude --version 2>&1 | grep -i remote
```

If you're on a Team or Enterprise plan, an admin must enable the Remote Control toggle in [Claude Code admin settings](https://claude.ai/admin-settings/claude-code).

### Wrapper not found

```bash
Expand Down Expand Up @@ -321,6 +378,15 @@ MIT License - see LICENSE file for details

## Changelog

### v3.1.0 (2026-03-19)

- **Auto Remote Control**: Every interactive `claude` invocation now automatically registers a named remote session
- Session named after the git repository root (or current directory outside a repo)
- Skipped automatically for non-interactive uses (`--print`, `--version`, subcommands, `--no-session-persistence`)
- Opt-out via `CLAUDE_NO_REMOTE_CONTROL=true`
- New module: `lib/remote-session.sh`
- New test suite: `tests/test-remote-session.sh` (22 tests)

### v3.0.0 (2026-02-01)

- Renamed from `claude-custom` to `claude-wrapper`
Expand Down
17 changes: 13 additions & 4 deletions bin/claude-wrapper
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ source "${WRAPPER_LIB}/binary-discovery.sh"
# shellcheck source=../lib/pre-launch.sh
source "${WRAPPER_LIB}/pre-launch.sh"

# shellcheck source=../lib/remote-session.sh
source "${WRAPPER_LIB}/remote-session.sh"

# Find and validate the real claude binary
CLAUDE_BIN=""
if ! CLAUDE_BIN="$(find_claude_binary "${WRAPPER_PATH}")"; then
Expand All @@ -49,6 +52,12 @@ fi
# Initialize secrets loader (discovers files, authenticates if needed)
init_secrets_loader

# Build remote control args (empty array if not applicable)
RC_ARGS=()
while IFS= read -r line; do
RC_ARGS+=("${line}")
done < <(build_remote_control_args "$@" || true)

# Launch claude with or without secrets injection
if secrets_available; then
# Inject secrets into environment
Expand All @@ -65,9 +74,9 @@ if secrets_available; then
fi
fi

debug_log "Executing: ${CLAUDE_BIN} [${#} args] with secrets loaded"
exec "${CLAUDE_BIN}" "$@"
debug_log "Executing: ${CLAUDE_BIN} [${#RC_ARGS[@]} rc args + ${#} args] with secrets loaded"
exec "${CLAUDE_BIN}" "${RC_ARGS[@]}" "$@"
else
debug_log "Executing: ${CLAUDE_BIN} [${#} args]"
exec "${CLAUDE_BIN}" "$@"
debug_log "Executing: ${CLAUDE_BIN} [${#RC_ARGS[@]} rc args + ${#} args]"
exec "${CLAUDE_BIN}" "${RC_ARGS[@]}" "$@"
fi
89 changes: 89 additions & 0 deletions lib/remote-session.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/usr/bin/env bash
# remote-session.sh - Automatic remote control for interactive claude sessions
# Requires: lib/logging.sh must be sourced first

# Derive a human-readable session name from the current git repo or directory
get_remote_session_name() {
local git_root
git_root="$(git rev-parse --show-toplevel 2>/dev/null)" || true

if [[ -n "${git_root}" ]]; then
basename "${git_root}"
else
basename "${PWD}"
fi
}

# Returns 0 if the args represent an interactive claude session (should inject --remote-control)
# Returns 1 if this looks like a non-interactive invocation
#
# Non-interactive signals:
# --print / -p : print mode
# --version : version check
# --help / -h : help output
# --remote-control/--rc: already has remote control
# --no-session-persistence: focused analysis task (scripts, CI)
# Any positional arg : subcommand (e.g. remote-control, mcp, help, update)
is_interactive_session() {
local found_double_dash=0
local saw_flag=0

for arg in "$@"; do
if [[ "${found_double_dash}" -eq 1 ]]; then
# After --, everything is positional — treat as non-interactive
return 1
fi

case "${arg}" in
--)
found_double_dash=1
;;
--print | -p | --version | --help | -h)
return 1
;;
--remote-control | --rc)
return 1
;;
--no-session-persistence)
return 1
;;
-*)
# Flag seen — subsequent bare words are values, not subcommands
saw_flag=1
;;
*)
# Bare positional arg: only a subcommand when it precedes all flags.
# Subcommands (remote-control, mcp, etc.) always come before flags.
# Flag values (e.g. the "hello" in -c "hello") always follow their flag.
if [[ "${saw_flag}" -eq 0 ]]; then
return 1
fi
;;
esac
Comment thread
sentry[bot] marked this conversation as resolved.
done

return 0
}

# Build extra args to prepend for remote control.
# Outputs nothing and returns 1 if remote control should not be injected.
# Outputs "--remote-control" and "session-name" (newline-separated) and returns 0 if it should.
build_remote_control_args() {
# Allow opt-out via environment variable
if [[ "${CLAUDE_NO_REMOTE_CONTROL:-}" == "true" ]]; then
debug_log "Remote control disabled via CLAUDE_NO_REMOTE_CONTROL"
return 1
fi

if ! is_interactive_session "$@"; then
debug_log "Non-interactive invocation detected, skipping remote control injection"
return 1
fi

local session_name
session_name="$(get_remote_session_name)"
debug_log "Injecting --remote-control with session name: ${session_name}"

printf '%s\n' "--remote-control" "${session_name}"
return 0
}
7 changes: 7 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Comprehensive test suite for the `claude-wrapper` wrapper script. Tests cover fu
```bash
# Run all tests
./tests/test-wrapper.sh
./tests/test-remote-session.sh

# With verbose output
VERBOSE=true ./tests/test-wrapper.sh
Expand Down Expand Up @@ -56,6 +57,12 @@ VERBOSE=true ./tests/test-wrapper.sh
- Environment variable passing
- Subprocess inheritance

### 7. Remote Session Tests (`test-remote-session.sh`)

- `is_interactive_session`: detects interactive vs. non-interactive invocations
- `get_remote_session_name`: derives name from git repo or directory
- `build_remote_control_args`: full injection logic including opt-out via `CLAUDE_NO_REMOTE_CONTROL`

## Test Output

### Success
Expand Down
Loading
Loading