Skip to content

Commit ebde124

Browse files
katspaughclaude
andauthored
fix: prevent hanging in non-interactive mode (#35)
* fix: prevent hanging in non-interactive mode Fix critical bugs where commands would hang indefinitely when run with --json or --quiet flags without required arguments. Critical Fixes: 1. getPassword(): Return null instead of prompting in non-interactive mode 2. account create: Require --chain-id in non-interactive mode 3. account create: Require --owners in non-interactive mode 4. account create: Require --threshold in non-interactive mode 5. account create: Require --name in non-interactive mode 6. account deploy: Require account argument in non-interactive mode Pattern Applied: ```typescript if (options.argument) { // Use provided argument } else { if (isNonInteractiveMode()) { outputError('Argument is required in non-interactive mode', ExitCode.INVALID_ARGS) } // Interactive prompt } ``` Impact: - Commands now fail fast with clear error messages instead of hanging - Better user experience for automation and CI/CD pipelines - Consistent error handling across all commands Testing: Running `safe --json account create` without required arguments now immediately errors with "Argument is required in non-interactive mode" instead of hanging indefinitely. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * docs: add comprehensive non-interactive usage section to README Add new section covering: - How to enable non-interactive mode (--json, --quiet) - Password handling for automation (env var, file, CLI flag) - Required arguments for each command - JSON output format and exit codes - Complete CI/CD bash script example - GitHub Actions workflow example - Best practices for automation - Command reference table with non-interactive support status This documentation helps users: - Set up automation workflows - Integrate Safe CLI into CI/CD pipelines - Handle passwords securely - Parse JSON output correctly - Understand exit codes for error handling 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent e154aea commit ebde124

File tree

4 files changed

+220
-0
lines changed

4 files changed

+220
-0
lines changed

README.md

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,205 @@ safe tx execute <txHash>
228228

229229
---
230230

231+
## 🤖 Non-Interactive Usage & Automation
232+
233+
Safe CLI supports non-interactive mode for automation, CI/CD pipelines, and scripting workflows.
234+
235+
### Enabling Non-Interactive Mode
236+
237+
Use the `--json` or `--quiet` global flags:
238+
239+
```bash
240+
# JSON output mode (machine-readable)
241+
safe --json [command]
242+
243+
# Quiet mode (suppresses prompts, minimal output)
244+
safe --quiet [command]
245+
```
246+
247+
### Password Handling for Automation
248+
249+
Commands requiring wallet signatures need passwords. Use these methods (in priority order):
250+
251+
**1. Environment Variable** (Recommended for CI/CD)
252+
```bash
253+
export SAFE_WALLET_PASSWORD="your-secure-password"
254+
safe --json tx sign 0xabc...
255+
```
256+
257+
**2. Password File** (Secure for scripts)
258+
```bash
259+
echo "your-secure-password" > ~/.safe-password
260+
chmod 600 ~/.safe-password
261+
safe --password-file ~/.safe-password tx sign 0xabc...
262+
```
263+
264+
**3. CLI Flag** (⚠️ Insecure - visible in process list)
265+
```bash
266+
safe --password "your-password" tx sign 0xabc...
267+
# Warning: Only use for testing!
268+
```
269+
270+
### Required Arguments
271+
272+
In non-interactive mode, commands require all arguments as flags:
273+
274+
#### Creating a Safe
275+
```bash
276+
safe --json account create \
277+
--chain-id 1 \
278+
--owners "0x123...,0x456..." \
279+
--threshold 2 \
280+
--name "my-safe" \
281+
--no-deploy
282+
```
283+
284+
#### Deploying a Safe
285+
```bash
286+
export SAFE_WALLET_PASSWORD="password"
287+
safe --json account deploy eth:0x742d35Cc...
288+
```
289+
290+
#### Signing a Transaction
291+
```bash
292+
export SAFE_WALLET_PASSWORD="password"
293+
safe --json tx sign 0xabc123...
294+
```
295+
296+
#### Executing a Transaction
297+
```bash
298+
export SAFE_WALLET_PASSWORD="password"
299+
safe --json tx execute 0xabc123...
300+
```
301+
302+
#### Getting Safe Info
303+
```bash
304+
safe --json account info eth:0x742d35Cc...
305+
```
306+
307+
#### Listing Transactions
308+
```bash
309+
safe --json tx list eth:0x742d35Cc...
310+
```
311+
312+
### JSON Output Format
313+
314+
All commands in `--json` mode return consistent JSON:
315+
316+
**Success:**
317+
```json
318+
{
319+
"success": true,
320+
"message": "Operation completed",
321+
"data": {
322+
"safeTxHash": "0x...",
323+
"address": "0x...",
324+
...
325+
}
326+
}
327+
```
328+
329+
**Error:**
330+
```json
331+
{
332+
"success": false,
333+
"error": "Error message",
334+
"exitCode": 4
335+
}
336+
```
337+
338+
**Exit Codes:**
339+
- `0` - Success
340+
- `1` - General error
341+
- `2` - Network error
342+
- `3` - Authentication failure
343+
- `4` - Invalid arguments
344+
- `5` - Configuration error
345+
- `6` - Safe not found
346+
- `7` - Wallet error
347+
348+
### Complete CI/CD Example
349+
350+
```bash
351+
#!/bin/bash
352+
set -e # Exit on error
353+
354+
# Set up environment
355+
export SAFE_WALLET_PASSWORD="${SAFE_PASSWORD}" # From CI secrets
356+
export SAFE_OUTPUT_FORMAT="json"
357+
358+
# Create a Safe
359+
RESULT=$(safe --json account create \
360+
--chain-id 11155111 \
361+
--owners "0x123...,0x456..." \
362+
--threshold 2 \
363+
--name "CI-Safe-$(date +%s)" \
364+
--no-deploy)
365+
366+
# Extract Safe address from JSON
367+
SAFE_ADDRESS=$(echo $RESULT | jq -r '.data.address')
368+
echo "Created Safe: $SAFE_ADDRESS"
369+
370+
# Deploy it
371+
safe --json account deploy sepolia:$SAFE_ADDRESS
372+
373+
# Get Safe info and verify deployment
374+
safe --json account info sepolia:$SAFE_ADDRESS | jq '.data.deployed'
375+
```
376+
377+
### GitHub Actions Example
378+
379+
```yaml
380+
name: Deploy Safe
381+
382+
on:
383+
push:
384+
branches: [main]
385+
386+
jobs:
387+
deploy:
388+
runs-on: ubuntu-latest
389+
steps:
390+
- name: Install Safe CLI
391+
run: npm install -g @safe-global/safe-cli
392+
393+
- name: Deploy Safe
394+
env:
395+
SAFE_WALLET_PASSWORD: ${{ secrets.SAFE_PASSWORD }}
396+
run: |
397+
safe --json account deploy sepolia:0x742d35Cc...
398+
```
399+
400+
### Best Practices
401+
402+
1. **Never commit passwords** - Use environment variables or CI secrets
403+
2. **Validate JSON output** - Check `success` field and handle errors
404+
3. **Use exit codes** - Script can check `$?` for command success
405+
4. **Parse with jq** - Extract specific fields from JSON output
406+
5. **Test locally first** - Use `--json` flag to test automation scripts
407+
6. **Use password files with proper permissions** - `chmod 600` for security
408+
409+
### Automation-Friendly Commands
410+
411+
| Command | Non-Interactive Support | Password Required |
412+
|---------|------------------------|-------------------|
413+
| `account create` | ✅ All args required | ❌ |
414+
| `account deploy` | ✅ Address required | ✅ |
415+
| `account info` | ✅ Address required | ❌ |
416+
| `account list` | ✅ No args needed | ❌ |
417+
| `account add-owner` | ✅ Address/owner required | ❌ (creates tx) |
418+
| `tx sign` | ✅ Hash required | ✅ |
419+
| `tx execute` | ✅ Hash required | ✅ |
420+
| `tx list` | ✅ Optional address | ❌ |
421+
| `tx status` | ✅ Hash required | ❌ |
422+
| `tx export` | ✅ Hash required | ❌ |
423+
| `tx import` | ✅ JSON required | ❌ |
424+
| `wallet create` | ✅ All args required | ❌ |
425+
| `wallet import` | ✅ All args required | ❌ |
426+
| `wallet list` | ✅ No args needed | ❌ |
427+
428+
---
429+
231430
## ⚙️ Configuration
232431

233432
### Storage Location

src/commands/account/create.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ export async function createSafe(options: SafeCreateOptions = {}) {
4949
// Use provided chain ID
5050
chainId = options.chainId
5151
} else {
52+
if (isNonInteractiveMode()) {
53+
outputError('Chain ID is required in non-interactive mode', ExitCode.INVALID_ARGS)
54+
}
5255
// Interactive chain selection
5356
const chains = Object.values(configStore.getAllChains())
5457
const selected = await p.select({
@@ -97,6 +100,9 @@ export async function createSafe(options: SafeCreateOptions = {}) {
97100
)
98101
}
99102
} else {
103+
if (isNonInteractiveMode()) {
104+
outputError('Owners are required in non-interactive mode', ExitCode.INVALID_ARGS)
105+
}
100106
// Interactive owner configuration
101107
const includeActiveWallet = await p.confirm({
102108
message: 'Include active wallet as an owner?',
@@ -187,6 +193,9 @@ export async function createSafe(options: SafeCreateOptions = {}) {
187193
outputError(`Threshold must be between 1 and ${owners.length}`, ExitCode.INVALID_ARGS)
188194
}
189195
} else {
196+
if (isNonInteractiveMode()) {
197+
outputError('Threshold is required in non-interactive mode', ExitCode.INVALID_ARGS)
198+
}
190199
// Interactive threshold input
191200
const threshold = await p.text({
192201
message: `Signature threshold (1-${owners.length}):`,
@@ -213,6 +222,9 @@ export async function createSafe(options: SafeCreateOptions = {}) {
213222
}
214223
safeName = options.name
215224
} else {
225+
if (isNonInteractiveMode()) {
226+
outputError('Name is required in non-interactive mode', ExitCode.INVALID_ARGS)
227+
}
216228
// Interactive name input
217229
const name = await p.text({
218230
message: 'Give this Safe a name:',

src/commands/account/deploy.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ export async function deploySafe(account?: string, options: DeploySafeOptions =
4040
outputError(error instanceof Error ? error.message : 'Invalid account', ExitCode.INVALID_ARGS)
4141
}
4242
} else {
43+
if (isNonInteractiveMode()) {
44+
outputError('Account address is required in non-interactive mode', ExitCode.INVALID_ARGS)
45+
}
4346
// Show interactive selection
4447
const undeployedSafes = safeStorage.getAllSafes().filter((s) => !s.deployed)
4548
if (undeployedSafes.length === 0) {

src/utils/password-handler.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { readFileSync } from 'fs'
22
import { resolve } from 'path'
33
import * as p from '@clack/prompts'
4+
import { isNonInteractiveMode } from './command-helpers.js'
45

56
export interface PasswordInput {
67
/** Password provided via CLI flag (least secure) */
@@ -63,6 +64,11 @@ export async function getPassword(
6364
}
6465

6566
// Priority 4: Interactive prompt (fallback)
67+
// Don't prompt in non-interactive mode - return null to trigger error
68+
if (isNonInteractiveMode()) {
69+
return null
70+
}
71+
6672
const password = await p.password({
6773
message: promptMessage,
6874
})

0 commit comments

Comments
 (0)