Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
0dc0cb2
Add OAuth `cld login` with PKCE loopback flow
const-cloudinary Jun 21, 2026
6887083
Make config persistence atomic and OAuth refresh concurrency-safe
const-cloudinary Jun 24, 2026
43fb67a
Add selectable default config, config inventory views, and OAuth toke…
const-cloudinary Jun 24, 2026
f7f57dc
Self-refreshing OAuth token; remove scattered refresh hooks
const-cloudinary Jun 25, 2026
18e2db4
Fix OAuth login defects: mutual -c/-C, busy port, config perms, refre…
const-cloudinary Jun 25, 2026
00f27d7
Handle non-interactive stdin/stdout for OAuth, confirms, and JSON output
const-cloudinary Jun 25, 2026
6218635
Run CI on macOS and Windows; make tests cross-platform
const-cloudinary Jun 25, 2026
1d77932
Fix Python 3.10 test failures: rename config command to avoid module …
const-cloudinary Jun 25, 2026
49aa977
Document OAuth login, default config, and token refresh in the README
const-cloudinary Jun 25, 2026
0598ba4
Drop --region from README OAuth docs (advanced use case)
const-cloudinary Jun 25, 2026
7f9a2f6
Untrack internal OAuth design/review docs
const-cloudinary Jun 25, 2026
0b4b9e6
Make OAuth client id and scopes env-overridable
const-cloudinary Jun 25, 2026
11efb4c
Bump minimum Python to 3.8
const-cloudinary Jun 25, 2026
9827027
Revoke OAuth token on logout; report default status on login
const-cloudinary Jun 25, 2026
04805e0
Make OAuth token refresh concurrency-safe
const-cloudinary Jun 28, 2026
ec335d5
Improve OAuth retry
const-cloudinary Jun 29, 2026
a355b15
Route all SDK calls through the OAuth 401-retry boundary
const-cloudinary Jun 29, 2026
d78a9f1
Render a branded, escaped OAuth login callback page
const-cloudinary Jun 30, 2026
765f10d
Clarify default-config messages on login and config -d
const-cloudinary Jun 30, 2026
84be16c
Isolate the developer's CLI config file from the test suite
const-cloudinary Jun 30, 2026
ad51e36
Require cloudinary>=1.44.4
const-cloudinary Jun 30, 2026
11b2818
Document pipx, uv, and pip install options in the README
const-cloudinary Jun 30, 2026
8c1cba5
Fix tests
const-cloudinary Jun 30, 2026
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
26 changes: 23 additions & 3 deletions .github/workflows/cloudinary-cli-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,32 @@ on:

jobs:
build:
# Run on every branch push, but avoid duplicate runs when a same-repo PR
# exists: same-repo changes run via the push event, while fork PRs (which
# can't trigger a push in this repo) run via the pull_request event.
if: >-
(github.event_name == 'push' && !github.event.pull_request.head.repo.fork) ||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork)

runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
# Full Python matrix on Linux; macOS and Windows get a single latest-Python smoke job each
# (enough to catch platform-specific regressions without 3x the runners and test clouds).
os: [ubuntu-latest]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
include:
- os: macos-latest
python-version: "3.14"
- os: windows-latest
python-version: "3.14"

# Git Bash ships on the GitHub Windows runners, so a single bash shell keeps every step
# identical across Linux, macOS, and Windows (no PowerShell variants to maintain).
defaults:
run:
shell: bash

steps:
- uses: actions/checkout@v4
Expand All @@ -35,7 +55,7 @@ jobs:
- name: Get test cloud
run: echo "CLOUDINARY_URL=$(bash tools/get_test_cloud.sh)" >> $GITHUB_ENV
- name: Show test cloud
run: echo $CLOUDINARY_URL | cut -d'@' -f2
run: echo "$CLOUDINARY_URL" | cut -d'@' -f2
- name: Test with pytest
run: |
pytest
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ venv

.cld-sync
.cld-settings
.cld-config
.venv
163 changes: 147 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,118 @@ It is fully documented at [https://cloudinary.com/documentation/cloudinary_cli](
## Requirements
Your own Cloudinary account. If you don't already have one, sign up at [https://cloudinary.com/users/register/free](https://cloudinary.com/users/register/free).

Python 3.6 or later. You can install Python from [https://www.python.org/](https://www.python.org/). Note that the Python Package Installer (pip) is installed with it.
Python 3.8 or later. You can install Python from [https://www.python.org/](https://www.python.org/). Note that the Python Package Installer (pip) is installed with it.

## Setup and Installation
## Installation

1. To install this package, run: `pip3 install cloudinary-cli`
2. To make all your `cld` commands point to your Cloudinary account, set up your CLOUDINARY\_URL environment variable. For example:
* On Mac or Linux:<br>`export CLOUDINARY_URL=cloudinary://123456789012345:abcdefghijklmnopqrstuvwxyzA@cloud_name`
* On Windows (cmd.exe):<br>`set CLOUDINARY_URL=cloudinary://123456789012345:abcdefghijklmnopqrstuvwxyzA@cloud_name`
* On Windows (PowerShell):<br>`$Env:CLOUDINARY_URL="cloudinary://123456789012345:abcdefghijklmnopqrstuvwxyzA@cloud_name"`
The CLI is published on PyPI as [`cloudinary-cli`](https://pypi.org/project/cloudinary-cli/). The package name (`cloudinary-cli`) is what you install; the command it provides is **`cld`** (it also installs a `cloudinary` alias). Pick the method that fits your setup. If you just want a working `cld` command and aren't sure, use **pipx** or **uv** — they install the CLI in its own isolated environment, so it won't conflict with other Python packages and you don't need to manage a virtual environment yourself.

### Option 1 — pipx (recommended)

[pipx](https://pipx.pypa.io) installs Python CLI tools into isolated environments and puts the `cld` command on your `PATH` automatically.

```sh
# Install pipx if you don't have it:
# macOS: brew install pipx && pipx ensurepath
# Debian/Ubuntu: sudo apt install pipx && pipx ensurepath
# Any platform: python3 -m pip install --user pipx && python3 -m pipx ensurepath

pipx install cloudinary-cli

# Upgrade later with:
pipx upgrade cloudinary-cli
```

After `pipx ensurepath`, open a new terminal so the updated `PATH` takes effect.

### Option 2 — uv

[uv](https://docs.astral.sh/uv/) is a fast Python package manager. Its `uv tool` command installs CLIs in isolation, like pipx:

```sh
uv tool install cloudinary-cli

# Upgrade later with:
uv tool upgrade cloudinary-cli
```

Or run it once without installing. The package's command is `cld`, so name it with `--from`:

```sh
uvx --from cloudinary-cli cld --help # uvx is shorthand for `uv tool run`
```

### Option 3 — pip

A plain `pip` install also works. Prefer a virtual environment so the CLI and its dependencies don't collide with your system or other projects:

```sh
python3 -m venv ~/.venvs/cloudinary-cli
source ~/.venvs/cloudinary-cli/bin/activate # Windows: .\.venvs\cloudinary-cli\Scripts\activate
pip install cloudinary-cli
```

To install without a virtual environment, use a per-user install (avoids needing `sudo` and keeps it out of system Python):

```sh
python3 -m pip install --user cloudinary-cli
```

If `cld` is not found afterwards, the user scripts directory is not on your `PATH`. See [Troubleshooting](#troubleshooting-the-cld-command).

### Option 4 — Docker (no Python needed)

If you'd rather not install Python at all, run the CLI from the official Docker image. See [Docker Usage](#docker-usage) below.

### Verify the installation

```sh
cld --version
```

### Troubleshooting the `cld` command

If your shell reports `cld: command not found` after installing:

- **pipx / uv:** run `pipx ensurepath` (or `uv tool update-shell`), then open a new terminal.
- **pip `--user` install:** the user scripts directory is not on your `PATH`. Find it with `python3 -m site --user-base` (the scripts live in its `bin` subdirectory on macOS/Linux, or `Scripts` on Windows) and add that to your `PATH`. For example, on macOS/Linux add this to `~/.zshrc` or `~/.bash_profile`:

```sh
export PATH="$PATH:$(python3 -m site --user-base)/bin"
```

- As a fallback, you can always invoke the CLI through Python: `python3 -m cloudinary_cli.cli <command>`.

## Configuration

Once installed, point your `cld` commands at a Cloudinary account using **either** of the following.

**Option A — Log in with OAuth (recommended).** Run:

```sh
cld login
```

This opens your browser to authorize the CLI, then saves the login as a configuration (named after the cloud) and sets it as the default. The CLI refreshes the token automatically, and you can remove the login at any time with `cld logout`.

**Option B — Set your `CLOUDINARY_URL` environment variable.** For example:

* On Mac or Linux:<br>`export CLOUDINARY_URL=cloudinary://123456789012345:abcdefghijklmnopqrstuvwxyzA@cloud_name`
* On Windows (cmd.exe):<br>`set CLOUDINARY_URL=cloudinary://123456789012345:abcdefghijklmnopqrstuvwxyzA@cloud_name`
* On Windows (PowerShell):<br>`$Env:CLOUDINARY_URL="cloudinary://123456789012345:abcdefghijklmnopqrstuvwxyzA@cloud_name"`

_**Note:** you can copy and paste your account environment variable from the Account Details section of the Dashboard page in the Cloudinary console._

3. Check your configuration by running `cld config`. A response of the following form is returned:
Then check your configuration by running `cld config`. A response of the following form is returned:

```
cloud_name: <CLOUD_NAME>
api_key: <API_KEY>
api_secret: ***************<LAST_4_DIGITS>
private_cdn: <True|False>
```
```
cloud_name: <CLOUD_NAME>
api_key: <API_KEY>
api_secret: ***************<LAST_4_DIGITS>
private_cdn: <True|False>
```

If you get an error message when running `cld config`, you may need to add your Python installation to your $PATH. To do so, you can run `PATH="$PATH:/Library/Python/Versions/3.8/bin"` in your terminal, and add `export PATH="$PATH:/Library/Python/Versions/3.8/bin"` to your `/.bash_profile` or `~/.zshrc`.
If `cld` itself is not found, see [Troubleshooting the `cld` command](#troubleshooting-the-cld-command).

## Quickstart

Expand All @@ -47,6 +137,8 @@ Usage: cld [cli options] [command] [command options] [method] [method parameters

```
cld --help # Lists available commands.
cld login # Logs in to a Cloudinary account via OAuth in your browser.
cld logout # Revokes and removes a saved OAuth login.
cld search --help # Shows usage for the Search API.
cld admin # Lists Admin API methods.
cld uploader # Lists Upload API methods.
Expand Down Expand Up @@ -243,7 +335,7 @@ Whereas using the saved configuration "accountx":
cld -C accountx admin usage
```

_**Caution:** Creating a saved configuration may put your API secret at risk as it is stored in a local plain text file._
_**Caution:** Creating a saved configuration may put your credentials at risk as they are stored in a local plain text file. This applies to both API-key configurations and OAuth logins._

You can create, delete and list saved configurations using the `config` command.

Expand All @@ -252,3 +344,42 @@ cld config [options]
```

For details, see the [Cloudinary CLI documentation](https://cloudinary.com/documentation/cloudinary_cli#config).

### Logging in with OAuth

Instead of saving an API key and secret, you can log in to a Cloudinary account through your browser. The CLI saves the resulting session as a named configuration and refreshes its token automatically.

```
cld login # Log in and save the configuration (named after the cloud).
cld login my-account # Save the login under a specific name.
cld logout # Choose a saved OAuth login to log out of.
cld logout my-account # Log out of a specific saved OAuth login.
```

The first login becomes the default automatically. When other configurations already exist, the new login is saved but not made the default; `cld login` tells you so and prints the command to make it the default. Once saved, an OAuth login is selected with `-C <name>` just like any other saved configuration.

`cld logout` revokes the login's token at the server and removes the saved configuration. If the token cannot be revoked (for example, you are offline), the saved configuration is still removed.

### Choosing a default configuration

The default configuration is used when no `-c`/`-C` option is given and no `CLOUDINARY_URL` environment variable is set. The first OAuth login becomes the default automatically; you can change it at any time.

```
cld config -d <name> # Set an existing saved configuration as the default.
cld config --unset-default # Clear the stored default.
cld config -ls # List saved configurations, marking the default and the active one.
```

When creating a configuration with `-n` or `--from_url`, add `--set-default` to make it the default in the same step. Resolution precedence is: `-c` (inline URL) > `-C` (saved name) > stored default > `CLOUDINARY_URL` environment variable.

### Refreshing OAuth tokens

OAuth tokens are refreshed automatically as needed, but you can refresh them manually.

```
cld config --refresh <name> # Refresh a saved OAuth configuration's token.
cld config --refresh-all # Refresh every saved OAuth configuration whose token is stale.
cld config --refresh <name> --force # Refresh even if the token is still fresh.
```

If a token can no longer be refreshed (for example, the login was revoked), the CLI reports the configuration and the `cld login` command to use to log in again.
Loading
Loading