Compare commits
2
Commits
v0.4.30-win
...
v0.4.32
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
292fc907fb | ||
|
|
0d117e97fe |
+16
-4
@@ -469,6 +469,7 @@ replaces the old Full Permissions on/off switch.
|
||||
| **Plan** | Proposes a plan and makes no changes | `--permission-mode plan` |
|
||||
| **Default** | Asks before each tool call | *(nothing — Claude Code's own default)* |
|
||||
| **Accept Edits** | Auto-approves file edits; other tools still prompt | `--permission-mode acceptEdits` |
|
||||
| **Auto** | A safety classifier approves routine actions and blocks risky ones, without prompting | `--permission-mode auto` |
|
||||
| **Bypass** | Auto-approves every tool call | `--dangerously-skip-permissions` |
|
||||
|
||||
New projects start in **Default**. Projects created before permission modes existed keep behaving
|
||||
@@ -480,12 +481,19 @@ the way they did: one that had Full Permissions on becomes **Bypass**, one that
|
||||
> has Docker socket access or reaches services on your network. The Overview tab tells you whether
|
||||
> the in-container sandbox is also on.
|
||||
|
||||
**Auto** sits between Accept Edits and Bypass: Claude Code's own classifier reviews each action,
|
||||
lets routine work through and blocks things that look risky (such as destructive or
|
||||
exfiltrating commands) — no prompts either way. Whether it is available depends on your Claude
|
||||
Code account, model and backend (local and OpenAI-compatible backends usually won't
|
||||
qualify). When it isn't available, Claude Code quietly starts in its normal prompting mode
|
||||
instead.
|
||||
|
||||
### When a change takes effect
|
||||
|
||||
- **Terminals** — the mode is applied when a terminal is opened, so it affects terminals you open
|
||||
from then on. A Claude session that is already running keeps the permissions it started with;
|
||||
close the tab and open a new terminal to change it. The badge on each terminal tab shows the mode
|
||||
that terminal was launched with (`plan`, `ask`, `edits`, `bypass`).
|
||||
that terminal was launched with (`plan`, `ask`, `edits`, `auto`, `bypass`).
|
||||
- **Resumed sessions** — a session resumed from the **Sessions** tab uses the project's current
|
||||
mode.
|
||||
- **Scheduled tasks** — these now honour the permission mode too (they previously always ran with
|
||||
@@ -494,8 +502,10 @@ the way they did: one that had Full Permissions on becomes **Bypass**, one that
|
||||
mode change to reach the scheduler.
|
||||
|
||||
> Scheduled tasks run headless (`claude -p`) and cannot answer a permission prompt. In any mode
|
||||
> other than **Bypass**, a task may simply stop early when Claude Code asks for approval. Its run
|
||||
> log records which mode it used.
|
||||
> other than **Auto** or **Bypass**, a task may simply stop early when Claude Code asks for
|
||||
> approval. In **Auto**, actions the classifier blocks are denied and the run carries on without
|
||||
> them — but if Auto isn't available for the project's model or backend, Claude Code falls back
|
||||
> to prompting and the task can stall the same way. Its run log records which mode it used.
|
||||
|
||||
---
|
||||
|
||||
@@ -1349,7 +1359,9 @@ Scheduled runs use the project's [permission mode](#permission-modes) — they n
|
||||
with `--dangerously-skip-permissions`. Because the mode travels into the container as an
|
||||
environment variable, **stop and start the project** after changing it for the scheduler to see the
|
||||
change. Remember that a headless run cannot answer a permission prompt, so in any mode other than
|
||||
**Bypass** a task may stop early when Claude Code asks for approval; the run log records the mode
|
||||
**Auto** or **Bypass** a task may stop early when Claude Code asks for approval (in Auto, blocked
|
||||
actions are denied instead, unless Auto is unavailable and Claude Code falls back to
|
||||
prompting); the run log records the mode
|
||||
that was used.
|
||||
|
||||
### Creating Tasks
|
||||
|
||||
@@ -114,7 +114,7 @@ progress modal.
|
||||
|
||||
## Permission Modes
|
||||
|
||||
`PermissionMode` in `models/project.rs` replaces the old `full_permissions` boolean. Four states,
|
||||
`PermissionMode` in `models/project.rs` replaces the old `full_permissions` boolean. Five states,
|
||||
mapped to CLI flags by `PermissionMode::cli_args()`:
|
||||
|
||||
| Mode | Serialized | CLI args passed to `claude` |
|
||||
@@ -122,6 +122,7 @@ mapped to CLI flags by `PermissionMode::cli_args()`:
|
||||
| **Plan** | `plan` | `--permission-mode plan` |
|
||||
| **Default** | `default` | *(none)* |
|
||||
| **Accept Edits** | `acceptEdits` | `--permission-mode acceptEdits` |
|
||||
| **Auto** | `auto` | `--permission-mode auto` |
|
||||
| **Bypass** | `bypass` | `--dangerously-skip-permissions` |
|
||||
|
||||
`Project.permission_mode` is `Option<PermissionMode>`; `effective_permission_mode()` falls back to
|
||||
@@ -531,7 +532,7 @@ Triple-C includes optional speech-to-text powered by [Faster Whisper](https://gi
|
||||
| `app/src/components/layout/StatusBar.tsx` | Project/terminal counts, Notes toggle, STT mic |
|
||||
| `app/src/components/projects/ProjectRow.tsx` | Select-only sidebar row; opens Project Home, with hover start/stop and terminal controls |
|
||||
| `app/src/components/projects/ProjectList.tsx` | Project list in sidebar |
|
||||
| `app/src/components/projects/PermissionModeControl.tsx` | Plan / Default / Accept Edits / Bypass segmented control |
|
||||
| `app/src/components/projects/PermissionModeControl.tsx` | Plan / Default / Accept Edits / Auto / Bypass segmented control |
|
||||
| `app/src/components/ui/` | Shared primitives: `Modal`, `Button`, `Toggle`, `Field`, `SegmentedControl`, `StatusIndicator`, `SaveIndicator`, `OverflowMenu`, `ToastHost`, `Tooltip` |
|
||||
| `app/src/hooks/useKeyboardShortcuts.ts` | `Ctrl+T`, `Ctrl+Shift+W`, `Ctrl+Tab`, `Ctrl+1..9`, `Ctrl+Shift+←/→` |
|
||||
| `app/src/hooks/useContainerProgress.ts` | `container-progress` event → inline progress lines |
|
||||
|
||||
+3
-2
@@ -186,7 +186,7 @@ host keychain secrets.
|
||||
|
||||
### Permission Modes
|
||||
|
||||
`PermissionMode` (`models/project.rs`) is a four-state enum replacing the earlier `full_permissions`
|
||||
`PermissionMode` (`models/project.rs`) is a five-state enum replacing the earlier `full_permissions`
|
||||
boolean. It reaches Claude Code by two different routes:
|
||||
|
||||
| Mode | `cli_args()` — interactive terminals | `as_env_value()` — scheduler |
|
||||
@@ -194,6 +194,7 @@ boolean. It reaches Claude Code by two different routes:
|
||||
| `Plan` | `--permission-mode plan` | `plan` |
|
||||
| `Default` | *(no flag)* | `default` |
|
||||
| `AcceptEdits` | `--permission-mode acceptEdits` | `acceptEdits` |
|
||||
| `Auto` | `--permission-mode auto` | `auto` |
|
||||
| `Bypass` | `--dangerously-skip-permissions` | `bypass` |
|
||||
|
||||
`Project.permission_mode` is `Option<PermissionMode>`, and `effective_permission_mode()` resolves
|
||||
@@ -474,7 +475,7 @@ triple-c/
|
||||
│ │ ├── ProjectRow.tsx # Select-only sidebar row
|
||||
│ │ ├── ProjectList.tsx # Sidebar project list
|
||||
│ │ ├── AddProjectDialog.tsx # New-project dialog
|
||||
│ │ ├── PermissionModeControl.tsx # Plan/Default/Accept Edits/Bypass
|
||||
│ │ ├── PermissionModeControl.tsx # Plan/Default/Accept Edits/Auto/Bypass
|
||||
│ │ ├── ConfirmRemoveModal.tsx # Project removal confirmation
|
||||
│ │ └── *Editor.tsx / *Modal.tsx # EnvVars, PortMappings,
|
||||
│ │ # ClaudeInstructions, ClaudeCodeSettings —
|
||||
|
||||
@@ -106,6 +106,16 @@ const CODE_REJECTED_EVENT: &str = "claude-token-code-rejected";
|
||||
/// browser, sign in, and approve. Bounded so a wedged exec can't leak a task.
|
||||
const SETUP_TIMEOUT: Duration = Duration::from_secs(15 * 60);
|
||||
|
||||
/// Pause between typing the pasted code and pressing Enter.
|
||||
///
|
||||
/// The CLI's prompt reads a multi-byte chunk as a paste, and a `\r` inside a
|
||||
/// paste is swallowed with it rather than submitting. Code and Enter in one
|
||||
/// write therefore fill the prompt and submit nothing, and the flow waits out
|
||||
/// [`SETUP_TIMEOUT`]. Measured against 2.1.283 under a pty: 20 ms apart
|
||||
/// already submits reliably; this leaves headroom for the extra hops through
|
||||
/// Docker's exec socket, which can merge writes that arrive close together.
|
||||
const SUBMIT_ENTER_DELAY: Duration = Duration::from_millis(250);
|
||||
|
||||
/// Documented shape of a `setup-token` credential.
|
||||
const TOKEN_PREFIX: &str = "sk-ant-oat01-";
|
||||
|
||||
@@ -837,9 +847,23 @@ unset CLAUDE_CODE_OAUTH_TOKEN ANTHROPIC_API_KEY ANTHROPIC_AUTH_TOKEN ANTHROPIC_B
|
||||
ANTHROPIC_MODEL CLAUDE_CODE_USE_BEDROCK AWS_BEARER_TOKEN_BEDROCK
|
||||
exec claude setup-token"#;
|
||||
|
||||
/// Type `code` into the CLI's prompt, then press Enter as a separate keystroke.
|
||||
///
|
||||
/// See [`SUBMIT_ENTER_DELAY`] for why the two cannot share a write.
|
||||
async fn type_code_then_enter<W: tokio::io::AsyncWrite + Unpin>(
|
||||
input: &mut W,
|
||||
code: &[u8],
|
||||
) -> std::io::Result<()> {
|
||||
input.write_all(code).await?;
|
||||
input.flush().await?;
|
||||
tokio::time::sleep(SUBMIT_ENTER_DELAY).await;
|
||||
input.write_all(b"\r").await?;
|
||||
input.flush().await
|
||||
}
|
||||
|
||||
/// Run `claude setup-token` in the container and return the token it printed.
|
||||
/// Streams redacted output as it arrives and forwards anything arriving on
|
||||
/// `input_rx` (the user's pasted code) to the command's stdin.
|
||||
/// `input_rx` (the user's pasted code) to the command's stdin, each followed by Enter.
|
||||
async fn run_setup_token(
|
||||
app: &AppHandle,
|
||||
project_id: &str,
|
||||
@@ -894,14 +918,13 @@ async fn run_setup_token(
|
||||
"Authentication cancelled. No token was stored.".to_string()
|
||||
);
|
||||
}
|
||||
Some(data) = input_rx.recv() => {
|
||||
if let Err(e) = input.write_all(&data).await {
|
||||
Some(code) = input_rx.recv() => {
|
||||
if let Err(e) = type_code_then_enter(&mut input, &code).await {
|
||||
return Err(format!(
|
||||
"Could not send the code to `claude setup-token`: {}. No token was stored.",
|
||||
e
|
||||
));
|
||||
}
|
||||
let _ = input.flush().await;
|
||||
// Arm the rejection detector. Anything the CLI says from here
|
||||
// on is a verdict on *this* code.
|
||||
awaiting_code_result = true;
|
||||
@@ -1158,10 +1181,9 @@ pub async fn submit_claude_token_code(code: String) -> Result<(), String> {
|
||||
.to_string()
|
||||
})?;
|
||||
|
||||
let mut keystrokes = code.as_bytes().to_vec();
|
||||
keystrokes.push(b'\r');
|
||||
// Just the code: the flow presses Enter itself, as a separate keystroke.
|
||||
sender
|
||||
.send(keystrokes)
|
||||
.send(code.as_bytes().to_vec())
|
||||
.map_err(|_| "The authentication flow has already ended.".to_string())
|
||||
}
|
||||
|
||||
@@ -2229,4 +2251,50 @@ mod tests {
|
||||
assert_eq!(outcome.snapshots_skipped.len(), 1, "{:?}", outcome);
|
||||
assert!(outcome.snapshots_failed.is_empty(), "{:?}", outcome);
|
||||
}
|
||||
|
||||
/// Records every `poll_write` as a separate entry, with when it landed, so
|
||||
/// a test can see write boundaries that a byte pipe would merge.
|
||||
#[derive(Default)]
|
||||
struct RecordingWriter {
|
||||
writes: Vec<(tokio::time::Instant, Vec<u8>)>,
|
||||
}
|
||||
|
||||
impl tokio::io::AsyncWrite for RecordingWriter {
|
||||
fn poll_write(
|
||||
mut self: std::pin::Pin<&mut Self>,
|
||||
_cx: &mut std::task::Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> std::task::Poll<std::io::Result<usize>> {
|
||||
self.writes
|
||||
.push((tokio::time::Instant::now(), buf.to_vec()));
|
||||
std::task::Poll::Ready(Ok(buf.len()))
|
||||
}
|
||||
fn poll_flush(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
_cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<std::io::Result<()>> {
|
||||
std::task::Poll::Ready(Ok(()))
|
||||
}
|
||||
fn poll_shutdown(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
_cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<std::io::Result<()>> {
|
||||
std::task::Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Code and Enter in one write is read by the CLI as a paste: the code
|
||||
/// fills the prompt and the `\r` is swallowed with it, so nothing is
|
||||
/// submitted and the flow sits until `SETUP_TIMEOUT`. Measured against
|
||||
/// 2.1.283 under a pty; a separate Enter 20 ms later submits.
|
||||
#[tokio::test(start_paused = true)]
|
||||
async fn the_code_and_its_enter_are_separate_writes() {
|
||||
let mut w = RecordingWriter::default();
|
||||
type_code_then_enter(&mut w, b"abc#def").await.unwrap();
|
||||
|
||||
assert_eq!(w.writes.len(), 2, "expected two writes, got {:?}", w.writes);
|
||||
assert_eq!(w.writes[0].1, b"abc#def");
|
||||
assert_eq!(w.writes[1].1, b"\r");
|
||||
assert!(w.writes[1].0 - w.writes[0].0 >= SUBMIT_ENTER_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -469,6 +469,20 @@ mod tests {
|
||||
assert!(!cmd[2].contains(" -n "), "empty name must add no flag: {}", cmd[2]);
|
||||
}
|
||||
|
||||
/// Auto mode is passed as a `--permission-mode` value, not its own flag.
|
||||
#[test]
|
||||
fn build_terminal_cmd_passes_auto_permission_mode() {
|
||||
let mut p = project("anthropic", serde_json::Value::Null);
|
||||
p.permission_mode = Some(crate::models::project::PermissionMode::Auto);
|
||||
let cmd = build_claude_terminal_cmd(&p, None, None);
|
||||
|
||||
assert!(
|
||||
cmd[2].contains("exec claude '--permission-mode' 'auto'"),
|
||||
"got: {}",
|
||||
cmd[2]
|
||||
);
|
||||
}
|
||||
|
||||
/// The Bedrock-profile path keeps its AWS validation *and* gains the
|
||||
/// prelude, immediately before the exec.
|
||||
#[test]
|
||||
|
||||
@@ -166,6 +166,9 @@ pub enum PermissionMode {
|
||||
Default,
|
||||
/// Auto-accept file edits, prompt for everything else.
|
||||
AcceptEdits,
|
||||
/// Claude Code's classifier approves safe actions and blocks risky ones,
|
||||
/// without prompting.
|
||||
Auto,
|
||||
/// Skip all permission prompts.
|
||||
Bypass,
|
||||
}
|
||||
@@ -180,6 +183,7 @@ impl PermissionMode {
|
||||
PermissionMode::AcceptEdits => {
|
||||
vec!["--permission-mode".to_string(), "acceptEdits".to_string()]
|
||||
}
|
||||
PermissionMode::Auto => vec!["--permission-mode".to_string(), "auto".to_string()],
|
||||
PermissionMode::Bypass => vec!["--dangerously-skip-permissions".to_string()],
|
||||
}
|
||||
}
|
||||
@@ -191,6 +195,7 @@ impl PermissionMode {
|
||||
PermissionMode::Plan => "plan",
|
||||
PermissionMode::Default => "default",
|
||||
PermissionMode::AcceptEdits => "acceptEdits",
|
||||
PermissionMode::Auto => "auto",
|
||||
PermissionMode::Bypass => "bypass",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ const MODE_BADGE: Record<PermissionMode, { text: string; className: string }> =
|
||||
plan: { text: "plan", className: "bg-[var(--bg-tertiary)] text-[var(--text-secondary)]" },
|
||||
default: { text: "ask", className: "bg-[var(--bg-tertiary)] text-[var(--text-secondary)]" },
|
||||
acceptEdits: { text: "edits", className: "bg-[var(--accent-muted)] text-[var(--accent)]" },
|
||||
auto: { text: "auto", className: "bg-[var(--accent-muted)] text-[var(--accent)]" },
|
||||
bypass: { text: "bypass", className: "bg-[var(--warning-muted)] text-[var(--warning)]" },
|
||||
};
|
||||
|
||||
|
||||
@@ -75,11 +75,11 @@ describe("PermissionModeControl", () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders all four modes as a radio group with the effective one checked", () => {
|
||||
it("renders all five modes as a radio group with the effective one checked", () => {
|
||||
render(<PermissionModeControl project={baseProject} onChange={onChange} />);
|
||||
const group = screen.getByRole("radiogroup", { name: "Permission mode" });
|
||||
expect(group).toBeInTheDocument();
|
||||
expect(screen.getAllByRole("radio")).toHaveLength(4);
|
||||
expect(screen.getAllByRole("radio")).toHaveLength(5);
|
||||
expect(screen.getByRole("radio", { name: "Default" })).toHaveAttribute(
|
||||
"aria-checked",
|
||||
"true",
|
||||
@@ -92,6 +92,14 @@ describe("PermissionModeControl", () => {
|
||||
expect(onChange).toHaveBeenCalledWith("acceptEdits");
|
||||
});
|
||||
|
||||
it("offers Auto between Accept Edits and Bypass", () => {
|
||||
render(<PermissionModeControl project={baseProject} onChange={onChange} />);
|
||||
const labels = screen.getAllByRole("radio").map((r) => r.textContent);
|
||||
expect(labels).toEqual(["Plan", "Default", "Accept Edits", "Auto", "Bypass"]);
|
||||
fireEvent.click(screen.getByRole("radio", { name: "Auto" }));
|
||||
expect(onChange).toHaveBeenCalledWith("auto");
|
||||
});
|
||||
|
||||
it("moves selection with the arrow keys", () => {
|
||||
render(<PermissionModeControl project={baseProject} onChange={onChange} />);
|
||||
fireEvent.keyDown(screen.getByRole("radiogroup", { name: "Permission mode" }), {
|
||||
|
||||
@@ -9,6 +9,11 @@ export const PERMISSION_MODES: Segment<PermissionMode>[] = [
|
||||
label: "Accept Edits",
|
||||
hint: "File edits are auto-approved; other tools still prompt.",
|
||||
},
|
||||
{
|
||||
value: "auto",
|
||||
label: "Auto",
|
||||
hint: "A safety classifier approves routine actions and blocks risky ones, without prompting.",
|
||||
},
|
||||
{
|
||||
value: "bypass",
|
||||
label: "Bypass",
|
||||
|
||||
@@ -159,12 +159,19 @@ describe("TaskEditorModal", () => {
|
||||
});
|
||||
|
||||
it("warns that a headless run cannot answer a permission prompt", async () => {
|
||||
// Bypass is the only mode where an unattended run is safe from stalling.
|
||||
// Bypass (and Auto, below) are the modes where an unattended run cannot stall.
|
||||
await renderEditor(null, { ...baseProject, permission_mode: "bypass" });
|
||||
expect(screen.getByText(/headless/i)).toBeInTheDocument();
|
||||
expect(screen.queryByText(/cannot answer a permission prompt/i)).toBeNull();
|
||||
});
|
||||
|
||||
it("tells Auto mode that blocked actions are denied, and warns of the fallback", async () => {
|
||||
await renderEditor(null, { ...baseProject, permission_mode: "auto" });
|
||||
expect(screen.queryByText(/cannot answer a permission prompt/i)).toBeNull();
|
||||
expect(screen.getByText(/blocks are denied/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/falls back to prompting/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("spells out the stall risk in any non-Bypass mode", async () => {
|
||||
await renderEditor(null, { ...baseProject, permission_mode: "default" });
|
||||
expect(screen.getByText(/cannot answer a permission prompt/i)).toBeInTheDocument();
|
||||
|
||||
@@ -301,11 +301,18 @@ export default function TaskEditorModal({ project, task, onClose, onSaved }: Pro
|
||||
terminal attached, using this project’s permission mode (
|
||||
<strong className="text-[var(--text-primary)]">{modeLabel}</strong>).
|
||||
</p>
|
||||
{mode !== "bypass" && (
|
||||
{mode === "auto" && (
|
||||
<p className="text-xs text-[var(--warning)]">
|
||||
In Auto mode, actions the safety classifier blocks are denied and the run carries on
|
||||
without them. If Auto isn’t available for this project’s model or backend,
|
||||
Claude Code falls back to prompting and the task may stall.
|
||||
</p>
|
||||
)}
|
||||
{mode !== "bypass" && mode !== "auto" && (
|
||||
<p className="text-xs text-[var(--warning)]">
|
||||
A headless run cannot answer a permission prompt. In {modeLabel} mode the task may
|
||||
stall and produce an empty log; set the mode to Bypass in the Config tab for
|
||||
unattended runs.
|
||||
stall and produce an empty log; set the mode to Auto or Bypass in the Config tab
|
||||
for unattended runs.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -126,7 +126,7 @@ export const CUSTOM_ENDPOINT_BACKENDS: readonly Backend[] = [
|
||||
];
|
||||
|
||||
/** Mirrors Rust `PermissionMode` (serde camelCase). */
|
||||
export type PermissionMode = "plan" | "default" | "acceptEdits" | "bypass";
|
||||
export type PermissionMode = "plan" | "default" | "acceptEdits" | "auto" | "bypass";
|
||||
|
||||
export type BedrockAuthMethod = "static_credentials" | "profile" | "bearer_token";
|
||||
|
||||
|
||||
@@ -62,11 +62,15 @@ TASK_TYPE=$(jq -r '.type' "$TASK_FILE")
|
||||
# PermissionMode::cli_args() in app/src-tauri/src/models/project.rs.
|
||||
# NOTE: headless `claude -p` runs cannot answer a permission prompt, so any
|
||||
# mode other than "bypass" means the task may stop early when Claude Code asks
|
||||
# for permission. Unset or unrecognized values pass no flag (Claude's default).
|
||||
# for permission. In "auto", classifier-blocked actions are denied instead of
|
||||
# prompted, but if auto mode is unavailable for the session's model/backend,
|
||||
# Claude Code falls back to prompting and the same stall applies.
|
||||
# Unset or unrecognized values pass no flag (Claude's default).
|
||||
PERMISSION_ARGS=()
|
||||
case "${TRIPLE_C_PERMISSION_MODE:-}" in
|
||||
plan) PERMISSION_ARGS=(--permission-mode plan) ;;
|
||||
acceptEdits) PERMISSION_ARGS=(--permission-mode acceptEdits) ;;
|
||||
auto) PERMISSION_ARGS=(--permission-mode auto) ;;
|
||||
bypass) PERMISSION_ARGS=(--dangerously-skip-permissions) ;;
|
||||
*) PERMISSION_ARGS=() ;;
|
||||
esac
|
||||
|
||||
Reference in New Issue
Block a user