curl commands reach you from a lot of directions. Browser DevTools has a "Copy as cURL" option on every request, most API references include a curl example, support tickets and bug reports might carry one, and coding assistants usually answer HTTP questions in curl. It's a sensible default, since curl is already installed on most machines and needs no setup.
The short answer
Paste the command into a client that reads curl natively. In Bruno: sidebar ··· on your collection → New Request → set Request type to From cURL → paste → Create. Method, URL, headers, and body come across, and the request is saved as a plain-text YAML file in your collection folder, not into a cloud workspace. To go the other way, open any request and use Generate Code to get curl back out. The rest of this guide maps exactly how much comes across, from auth and multipart uploads to cookies and compression, and how to get back to the terminal whenever you want.
A curl GUI is a visual HTTP client that can read and write curl commands. You paste a command in, it becomes an editable request with tabs for headers, body, and auth; you click a button, and you get a runnable curl command back. The distinction that matters isn't just the graphical interface. It's also whether the conversion runs both directions, and what format your requests are stored in once they're inside.
Tools that import curl but can't export it, or that store your requests in a proprietary cloud workspace, are a one-way door. That's the thing developers who live in the terminal are often objecting to when they say they don't want a GUI.
Two things make working with curl commands awkward. The first shows up on a single command, before any accumulation happens. Real commands get complicated fast: a dozen headers, a nested JSON body, an auth flag, and a couple of query params, all flattened into one line where a missing brace or a quote in the wrong position is genuinely hard to see. Reading it means parsing shell syntax in your head, and changing one value means editing a string rather than a field. An interactive client splits the same command into headers, body, auth, and params, so you can see what's actually being sent and adjust one part without disturbing the rest. That's useful even if you only ever run the command once.
The second is storage. A command worth running twice ends up in your shell history, a scratch file, or a Slack thread, none of which you can search reliably when you need it again a month later.
That isn't a case against curl. It's a case for having somewhere to open a command up, see its parts clearly, and keep it around if it turns out to be worth keeping.
The flow takes just a few clicks:
··· on your collection → New Request.Bruno opens the new request with the method, URL, headers, and body already filled in. Multi-line commands with trailing backslashes paste fine, so you don't need to flatten them first.
Both problems from the last section get handled at once. The command is now a set of editable fields, with headers, body, auth, and params each in their own tab, so you can read any one of them without picking apart a long line and change one without disturbing the rest. It's also a plain-text YAML file inside your collection folder, which you can open in your editor, grep, diff, and commit. Nothing gets uploaded, and there's no account to create, which also means the import works on an air-gapped machine.
Try it with a command you already have. Download Bruno. It's open source, MIT-licensed, and your collections stay on your machine.
Most "paste your curl here" boxes are vague about how much of your command they actually understand. Here's the specific mapping:
| curl flag | What Bruno does with it |
|---|---|
-X, --request |
Sets the HTTP method |
-H, --header |
Adds a header |
-A, --user-agent |
Sets the User-Agent header |
-d, --data, --data-ascii, --data-urlencode |
Becomes the request body; repeated flags are joined with & |
--data-raw |
Becomes the request body, subject to the same parsing as -d |
--data-binary |
@payload.json becomes a file body, taking its content type from the command's Content-Type header |
--json |
JSON body, sets Content-Type: application/json, and promotes GET/HEAD to POST |
-F, --form |
Multipart form body; @path/to/file entries become file parts |
-u, --user |
Username and password land in the Auth tab as Basic auth |
--digest, --ntlm |
Alongside -u, switches the auth mode to Digest or NTLM |
-b, --cookie |
Becomes a Cookie header |
-G, --get |
Moves -d data into query parameters |
-I, --head |
Method becomes HEAD |
--compressed |
Adds Accept-Encoding: deflate, gzip |
-k, --insecure |
Recognized, but not applied to the request. TLS verification is a client setting, not a per-request flag |
One behavior sits underneath several of those body rows. For a form body, or a request with no Content-Type at all, the data string is parsed into individual fields. For a structured content type such as application/json, it's kept as a single raw string. That's why --data-raw 'raw&stuff' with no Content-Type arrives as two empty fields, while the same flag alongside Content-Type: application/json arrives untouched.
Three conveniences worth knowing about, because they're what usually breaks a naive parser:
curl api.example.com/users imports as a real URL instead of failing, the same way curl itself would.-XPOST is handled. The concatenated form gets normalized to -X POST.Flags that describe how curl connects rather than what the request is are ignored on import:
-L / --location, -o / -O, -x / --proxy, --cert / --key / --cacert, --max-time, --connect-timeout, --retry, -v, -T
That's a deliberate split. Redirect behavior, proxies, client certificates, timeouts, and TLS verification are environment concerns. In Bruno they live in the app's preferences and collection settings, set once and applied to every request in the collection, instead of being copy-pasted into each one.
One thing worth knowing if the command used -k. That flag tells curl to skip TLS certificate verification, which is how a command keeps working against a self-signed or expired certificate on a staging box. Since it isn't applied to the imported request, the same request will fail on Send with a certificate error. Turn off SSL/TLS Certificate Verification in Preferences to get the same behavior, bearing in mind that it's an app-wide setting rather than a per-request one.
Whatever the command contained, give the imported request a quick once-over before you rely on it. Read the Body, Headers, and Auth tabs against the original and confirm they say what you expect. Form bodies deserve the closest look, since characters like &, +, and = are structural there rather than literal, so a value carrying one can end up read as syntax instead of data. Sending once against an echo endpoint such as https://echo.usebruno.com or https://httpbin.org/post shows you exactly what left the client, which settles the question faster than reading the request pane ever will.
This is the half many tools skip, and it's the half that decides whether importing was safe.
Open a request in Bruno and click the </> icon in the url bar to open Generate Code, then pick your language. curl is in the list, alongside 35+ others (Python, JavaScript, Go, and so on). It's often the fastest way to answer "what exactly did you send?" in a bug report, a support ticket, or a docs example, outside of sharing a Bruno workspace or collection itself. You build the request in the GUI where iterating is cheap, then hand over a one-liner anyone can run. Full details are in the code generator documentation.
One habit worth keeping: a generated snippet contains whatever your request contains. If the header holds a real token, the curl you just copied holds it too. Swap in a placeholder before it goes anywhere public. Variables are helpful here.
Once a request is created, the more useful "back to terminal" move usually isn't exporting a single command. It's having a way of running the collection programmatically:
npm install -g @usebruno/cli
cd my-collection
bru run --env staging
Same requests, same assertions, no GUI required.
For CI you don't have to hand-install anything. On GitHub Actions there's an official action:
- uses: usebruno/bruno-cli-action@v1
with:
working-directory: collections/api
command: 'run --env ci --reporter-junit results.xml'
Everywhere else, including GitLab CI, Jenkins, Azure Pipelines, and a local pre-push hook, there's an official Docker image with bru as the entrypoint. Mount the collection at /bruno and everything after the image name goes straight to bru:
docker run --rm -v "$(pwd):/bruno" usebruno/cli run --env ci --reporter-junit results.xml
Pin both to a specific release rather than a floating tag, so a pipeline that passes today passes for the same reasons next month. The image is also on GHCR as ghcr.io/usebruno/cli, runs non-root, and is multi-arch. Both are covered in detail in the post on the official Docker image and GitHub Action.
All three are the same bru run underneath, which means the command you debug on your laptop is the command CI executes. The GUI becomes the place you author requests; the terminal stays the place you run them. (More on that in the Bruno CLI guide.)
The collection is a folder of plain YAML files. You can open them up and read them if you wanted. git diff shows a changed endpoint as a changed line. A new request shows up in a pull request as a new modification. Whatever you imported this morning is reviewable by a teammate this afternoon, which is the whole premise behind keeping API collections in Git.
An imported curl command is a starting point, not a finished test. The upgrade path is short:
/users instead of https://api-staging.example.com/users, so the same request runs against staging and prod. (How to manage variables in Bruno.)Authorization header that a browser handed you. (Managing secrets in Bruno.)bru run, so a breaking change shows up in a pull request instead of in production.test("returns 200", function() {
expect(res.status).to.equal(200);
});
None of these steps takes long, and each one removes a different limitation. Variables free the request from a single environment, secret variables keep credentials out of the repo, an assertion turns a request you have to read into one that reports on itself, and committing it makes any of that reviewable. A curl command in your shell history has none of them.
| What you need | curl alone | Cloud-first GUI | Bruno |
|---|---|---|---|
| One-off exploratory request | Ideal | Fine | Fine |
| Re-run the same request weekly | Shell history archaeology | Good | Good |
| Import an existing curl command | n/a | Usually supported | ✓ natively |
| Export back to curl | n/a | Varies | ✓ Generate Code, 35+ languages |
| Share with a teammate | Paste a command | Invite them to a workspace | Send a file, or git pull |
| Review API changes in a PR | n/a | Usually not visible in Git | Plain-text YAML diff |
| Where secrets live | Shell history | The vendor's cloud | Local secret vars, gitignored |
| Run in CI | Bash scripts you maintain | Varies, often paid | bru run |
| Works offline / air-gapped | ✓ | Usually not | ✓ |
| Account required | No | Usually | never |
| License | Open source (curl) | Typically proprietary | Open source (MIT) |
The honest summary: curl is not the thing to replace. It's the thing to interoperate with. A visual client earns its place by making the round trip cheap in both directions, and by storing your requests as files you could still read, run, and hand to someone else if you stopped using that client tomorrow.
Whichever client you land on, these are the questions worth asking before you paste anything into it:
-u, --digest, and --ntlm are where most parsers quietly drop information.On Windows, curl is an alias for Invoke-WebRequest unless you call curl.exe explicitly. If a command copied from documentation errors out before you can even import it, that's usually why. In Chrome DevTools, choose Copy as cURL (bash) rather than the cmd variant for the cleanest import.
"Copy as cURL" from DevTools includes every header the browser sent: sec-ch-ua, sec-fetch-*, Referer, and the whole cookie jar. Import it, then clean up by deleting everything that isn't actually required by the API. Most endpoints need a handful of headers, not twenty.
The Cookie header in a browser-copied command is often your live login, and you usually have no way of knowing how long it stays valid. Some expire within the hour; others keep working for months. Both cases cause problems, in opposite directions. A short-lived one makes the request start failing later for reasons that have nothing to do with the request itself. A long-lived one is a working key to your account sitting in a file, still valid for whoever reads it next. Treat it as a real credential either way: replace it with proper auth on the Auth tab, or move it to a secret variable if you genuinely need it.
The new request creation From cURL flow creates a single request per paste. If you're facing a file with fifty commands in it, importing an OpenAPI spec, or generating the YAML request files directly, or having AI import for you will be faster than fifty pastes. And if your curl commands started life inside a Postman collection, import the collection instead: the Postman to Bruno migration guide covers scripts, environments, and the conversion errors worth knowing about up front.
Create a new request in Bruno: sidebar ··· on the collection → New Request → set Request type to From cURL → paste the command → Create. The method, URL, headers, and body are filled in automatically.
No. Bruno stores collections as files on your filesystem, so the import works offline and nothing is uploaded.
Yes. Open the request, click the </> icon for Generate Code, and select cURL. The same menu generates snippets for 35+ languages.
Yes. Bruno runs on Windows, macOS, and Linux. On Windows, remember that PowerShell's curl is an alias for Invoke-WebRequest, so copy commands as cURL (bash) from DevTools for a clean import.
Yes. -F name=@file.png becomes a multipart file part, and --data-binary @payload.json becomes a file body.
It's recognized but not applied to the imported request. TLS verification is configured in the client's settings rather than per request.
Yes. Credentials land in the request's Auth tab as Basic auth. Add --digest or --ntlm and the mode switches accordingly.
Not in a single paste. From cURL creates one request at a time. For bulk work, import an OpenAPI spec or a Postman collection, or write the YAML request files directly using AI.
Grab any curl command lying around, whether from browser DevTools, an API doc, or your shell history, and import it. Then generate the curl back out and compare. If the round trip is lossless in the ways that matter to you, the GUI isn't a one-way door.
Download Bruno free: open source, offline-first, no account required.
Coming from another client? See how Bruno compares against Postman and against Thunder Client.
Related reading: Postman to Bruno migration guide · Bruno CLI · Docker image and GitHub Action · Managing variables · Managing secrets