Opt-in wrapper for permission-bypass queries.
Running the claude CLI with --dangerously-skip-permissions turns
off every confirmation prompt for tool use. It's legitimate for some
automation -- but it's also the fastest way to turn a bug into a
destructive action.
This module isolates that capability behind a type you have to
explicitly reach for (ClaudeWrapper.DangerousClient) and a runtime
env-var gate (CLAUDE_WRAPPER_ALLOW_DANGEROUS) you have to
explicitly set.
Usage
# At process start (deliberately):
# export CLAUDE_WRAPPER_ALLOW_DANGEROUS=1
config = ClaudeWrapper.Config.new(working_dir: "/path/to/project")
case ClaudeWrapper.DangerousClient.new(config) do
{:ok, client} ->
query = ClaudeWrapper.Query.new("clean up the build artifacts")
{:ok, result} = ClaudeWrapper.DangerousClient.query_bypass(client, query)
{:error, %ClaudeWrapper.Error{kind: :dangerous_not_allowed} = error} ->
# The caller forgot to set the env var; refuse loudly rather
# than silently running with (or without) bypass.
{:error, error}
endWhy this shape
- Separate type.
new/1is the only path to building a bypassed query through this module. If a reader of calling code seesDangerousClient, the danger is obvious at the call site. - Runtime env-var gate. The check happens at construction, so a caller who forgot to set the env var gets a tagged error rather than silently running with bypass off (which might surprise them) or silently running with bypass on (which might destroy things).
- Checked on every construction. The gate is read on each call
to
new/1rather than memoized, so a test that flips the env var mid-process sees the change.
The lower-level ClaudeWrapper.Query.dangerously_skip_permissions/1
setter is still available without this gate; this module is the
guarded, intention-revealing path on top of it.
Equivalent to the Rust crate's dangerous::DangerousClient. The Rust
type splits query_bypass into async (query_bypass) and blocking
(query_bypass_sync) variants gated by cargo features; the Elixir
port has a single synchronous query_bypass/2 since Query.execute/2
is synchronous and there are no feature gates.
Summary
Types
A guarded client. Holds the shared ClaudeWrapper.Config.t/0 that
bypass queries run against.
Types
@type t() :: %ClaudeWrapper.DangerousClient{config: ClaudeWrapper.Config.t()}
A guarded client. Holds the shared ClaudeWrapper.Config.t/0 that
bypass queries run against.
Functions
@spec allow_env() :: String.t()
The name of the env var that must equal "1" for new/1 to succeed.
Exposed so callers and tests can reference the gate without hardcoding the string.
Examples
iex> ClaudeWrapper.DangerousClient.allow_env()
"CLAUDE_WRAPPER_ALLOW_DANGEROUS"
@spec config(t()) :: ClaudeWrapper.Config.t()
Return the underlying config for composition with other wrapper APIs.
@spec new(ClaudeWrapper.Config.t()) :: {:ok, t()} | {:error, ClaudeWrapper.Error.t()}
Build a guarded client, refusing unless the opt-in env var is set.
Succeeds only when System.get_env("CLAUDE_WRAPPER_ALLOW_DANGEROUS") == "1". Otherwise
returns {:error, %ClaudeWrapper.Error{kind: :dangerous_not_allowed}},
whose :reason is the name of the env var to set.
The check is made on each call rather than memoized, so a process that sets the env var after start (or a test that flips it) is honored.
Examples
{:ok, client} = ClaudeWrapper.DangerousClient.new(ClaudeWrapper.Config.new())
{:error, %ClaudeWrapper.Error{kind: :dangerous_not_allowed, reason: "CLAUDE_WRAPPER_ALLOW_DANGEROUS"}} =
ClaudeWrapper.DangerousClient.new(ClaudeWrapper.Config.new())
@spec query_bypass(t(), ClaudeWrapper.Query.t()) :: {:ok, ClaudeWrapper.Result.t()} | {:error, term()}
Run query with --dangerously-skip-permissions set, against the
client's config.
Sets dangerously_skip_permissions on the query (replacing any prior
value) and delegates to ClaudeWrapper.Query.execute/2. Returns
whatever execute/2 returns: {:ok, %ClaudeWrapper.Result{}} or
{:error, reason}.