# dsh-claude-code-local

A **local Claude Code** provider for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)
(DSH): a Cordis plugin that registers the `claude-code` provider route on
`ctx.llm` and serves every model request by running your locally installed
Claude Code CLI (`claude -p`) in streaming JSON mode.

- The provider appears in the model picker (group **Claude Code (local)**) and
  on the Models settings page — that is the "choose a provider" surface.
- The harness conversation is rendered to a plain-text prompt and piped to the
  CLI over stdin (long histories never hit OS argument-length limits).
- Claude Code runs its **own agentic loop** (its built-in tools, permission
  mode, max turns) inside each request; streamed text deltas are translated
  into harness chunks in real time, and internal tool activity is surfaced as
  a reasoning block so long runs show liveness.
- No API key is needed in DSH: the CLI authenticates with its own stored
  credentials (`~/.claude`, `ANTHROPIC_API_KEY`, OAuth, Bedrock/Vertex, …).
- macOS/Linux only.

## Requirements

- DeepSeek Harness `0.1.0-rc.7` or newer (web profile, headless profile, or
  the DSH Desktop app).
- The Claude Code CLI (`claude`) installed and authenticated — `claude --version`.

## Installation

### Option A — DSH Desktop community market (recommended on the desktop app)

1. Open the **Market** page in DSH Desktop.
2. **Sources** → add a source with the manifest URL:

   ```
   https://raw.githubusercontent.com/NGdust/dsh-claude-code-local/main/catalog/manifest.json
   ```

3. Select the source, open **Discover**, find **Claude Code (local)**, and
   install it. Restart the desktop app when prompted.

### Option B — dsh CLI (web / headless profiles)

```sh
dsh plugin --profile web add dsh-claude-code-local
dsh --profile web        # restart the server
```

### Option C — manual (no pnpm / no npm registry)

Copy the package into the profile module farm and register the plugin row in
the profile patch layer:

```sh
# 1. copy the package (this repository) into the profile's module farm
cp -R dsh-claude-code-local ~/.dsh/profiles/node_modules/

# 2. register the plugin row in the profile patch layer
#    ~/.dsh/profiles/web/cordis.patch.yml:
#    - insert:
#        - id: llm-claude-code
#          name: dsh-claude-code-local
```

The profile patch is hot-watched: a running web app picks the row up without a
restart. If it does not, restart `dsh web`. The DSH Desktop app applies profile
patch changes on the next launch.

## Configuration

All values are optional; defaults are shown. The settings section is
hot-reloaded from `$DSH_HOME/settings.yaml` (a plugin restart is not needed):

```yaml
llm-claude-code:
  executable: claude          # path/name of the Claude Code CLI
  model: ""                   # "" = use the picker selection; "default" = omit --model
  permissionMode: auto        # auto | acceptEdits | bypassPermissions | manual | dontAsk | plan
  allowedTools: []            # e.g. ["Bash(git *)", "Read"]
  disallowedTools: []
  tools: ""                   # raw --tools override; "" disables Claude's own tools
  maxTurns: 10                # internal agent steps per request
  appendSystemPrompt: true    # append the harness system prompt to Claude's own
  cwd: ""                     # CLI working directory; unset inherits the server cwd
  noSessionPersistence: true  # don't write Claude Code session files to disk
  extraArgs: []               # advanced: extra raw CLI arguments
  streamIdleTimeoutMs: 300000 # idle watchdog between streamed events
  models:
    - id: claude-opus-5
      name: Claude Opus 5
      contextWindow: 200000
    - id: claude-sonnet-5
      name: Claude Sonnet 5
      contextWindow: 200000
    - id: claude-haiku-4-5
      name: Claude Haiku 4.5
      contextWindow: 200000
    - id: claude-fable-5
      name: Claude Fable 5
      contextWindow: 200000
```

The default catalog is the current lineup as resolved by the installed CLI
(`opus` → `claude-opus-5`, `sonnet` → `claude-sonnet-5`, `haiku` →
`claude-haiku-4-5`, `fable` → `claude-fable-5`). The `models` list is advisory
(what the picker offers); the CLI decides which models your account can
actually serve. A model the picker does not list is still served if it reaches
the adapter as the session selection.

### Make it the default provider

New sessions default to the profile's `agent-default-model` (DeepSeek). To
default every new session to local Claude Code instead, add:

```yaml
agent-default-model:
  provider: claude-code
  model: claude-sonnet-5
```

Or pick it per session with the composer's model selector.

### Permission modes

- `auto` (default): Claude Code's auto-mode classifier — safe actions run,
  risky ones are denied in non-interactive mode.
- `acceptEdits`: file edits are auto-approved.
- `bypassPermissions`: **dangerous** — Claude Code can do anything its tools
  allow. Equivalent to `--dangerously-skip-permissions`.

## How it works

1. `apply()` registers the `claude-code` route with a `ClaudeCodeAdapter` and
   declares it in the configurable-provider directory, so the model picker and
   Models page know it.
2. Each request spawns `claude -p --output-format stream-json
   --include-partial-messages --verbose <flags>` with the rendered conversation
   on stdin.
3. `stream_event`/`result` JSON events are translated into harness
   `StreamChunk`s: `text_delta` → `text-delta`, `thinking_delta` →
   `reasoning-delta`, `content_block_start` tool_use → a reasoning liveness
   line, `result` usage → `TokenUsage`, and a terminal `finish`.
4. The child is killed on caller abort or idle timeout, and an idle watchdog
   fails the request when the CLI goes silent for `streamIdleTimeoutMs`.

## Notes and limitations

- Each request is a fresh stateless CLI invocation; the full conversation is
  re-rendered every turn (no cross-request prompt-cache reuse).
- The harness's own tools are not used through this provider: Claude Code owns
  its loop, so DSH's sandbox/permission service does not gate Claude Code's
  actions — the CLI's `permissionMode` does. Choose it deliberately.
- `maxTokens`/`temperature`/`stop` in the harness request are ignored; the CLI
  controls sampling.

## Development

```sh
npm pack                 # build the publishable tarball (also validates files)
npm publish              # publish to the npm registry (requires npm auth)
```

## License

MIT
