Skip to content
Scalpels home

Scalpels is officially launching on October 31, 2026. Special early adopter pricing is available now.

← All guides

Guide

A Comprehensive Agent's Guide to Laravel Cloud

Run commands on Cloud

· Ed Grosvenor

Sooner or later you need to run something inside a Cloud environment: a migration status check, a query, a one-off fix. There are two channels, cloud command:run and cloud tinker. Both work. Both report results in ways that are easy to misread.

command:run runs a shell command

cloud command:run <environment-id> --cmd='php artisan migrate:status' --json -n --fields=id,status,exitCode,output
  • The command runs in bash, not artisan. You must include php artisan. A bare migrate:status fails with command not found, and the result still says command.success.
  • The environment is a positional argument. The global --env option is not a Cloud environment and is silently ignored.
  • One command per call, passed with --cmd.
  • It runs on one instance. In an environment with several replicas, it lands on one of them. Anything you write to that instance's disk is not on the others.
  • It fails while a deploy is in progress. Check that no deployment is running before you start, and retry after it finishes.

Read exitCode, never status

The status describes delivery. The exit code describes what happened.

Status Meaning
pending, command.created, command.running Not finished yet.
command.success Cloud ran the command. It says nothing about whether the command worked.
command.failure Cloud could not run it. There may be no exit code at all.

We have seen command.success alongside exitCode: 1. The CLI's own process exit status follows the Cloud status, not the remote exit code, so cloud command:run … && echo ok prints ok for a command that failed. Check exitCode. If it is null or missing, you do not know the result: do not invent one.

Parse the output as a stream

With --json and no terminal, command:run waits for the command and writes one JSON object per line each time the status changes, then the final command record:

{"command_id": "comm-…", "status": "command.running", "message": "Running"}
{"command_id": "comm-…", "status": "command.success", "message": "Success"}
{"id": "comm-…", "status": "command.success", "exitCode": 0, "output": "…"}

That whole stream is not one JSON document, so decoding it in one go fails. Read it line by line and take the last object that has an exitCode key. Older CLIs returned a single {"id": …, "status": "pending"} object and exited, leaving you to poll cloud command:get <id>. If your wrapper expects one shape and gets the other, it will report a failure for a command that already ran. That left an orphaned row behind for us once.

Some practicalities:

  • The final record carries the environment record, including build commands. Always pass --fields. See Keep credentials out of your context.
  • output can contain raw control characters. Parse with a lenient decoder, such as Python's json.loads(line, strict=False).
  • The CLI waits indefinitely. It polls every three seconds with no timeout of its own. Put your own timeout around it, and make it generous: a remote command can take over a minute end to end. If your wrapper uses Laravel's Process facade, remember its default timeout is 60 seconds.
  • --no-monitor returns as soon as the command is submitted, with exit status 0. That proves nothing about the result. Use it only if you then poll cloud command:get <id> --json -n --fields=status,exitCode,output until the status is final.
  • Everything is retained. The command line and output are stored in Cloud's command history. Never print a secret.

Getting PHP through the shell

--cmd goes through a shell, so PHP's ->, =>, $ and quotes are mangled long before tinker sees them. Do not fight the escaping. Encode the script:

code=$(base64 < fix.php | tr -d '\n')
cloud command:run <environment-id> \
  --cmd="php artisan tinker --execute=\"eval(base64_decode('${code}'));\"" \
  --json -n --fields=status,exitCode,output

fix.php contains plain PHP statements without an opening <?php tag. For a longer script, decode it to a file in /tmp first and require it, remembering that the file exists only on the instance that ran the command.

cloud tinker sends PHP without a shell

From v0.6.0, cloud tinker sends code to Cloud's code execution API instead of a shell, so there is no shell escaping to get wrong:

cloud tinker <environment-id> --code='return App\Models\User::count();' --timeout=120 --json -n
  • --code is required when you are non-interactive.
  • --timeout defaults to 60 seconds. When it expires, the CLI stops waiting. The code may still be running.
  • Treat its output as retained, like command:run.

Older notes say there is no cloud tinker. That was true of the old source checkout, not of current releases.

Before any remote write

  • Resolve the environment ID in this session and pass it positionally. On CLIs before v0.6.2, an unknown ID ran the command on a different environment.
  • Prefer read-only commands while investigating: SELECT, counts, migrate:status, about.
  • For a destructive change, wrap it in a transaction, print what it will change and stop. Run the change only after you, or the human, have checked the preview.
  • Afterwards, verify with a separate read rather than trusting the write's own output.

Want some help?

After you buy, we will pay for an hour with one of these experts to help you get everything up and running.

  • Artisan Build

    USA

    We run Scalpels, as well as a suite of other tools for developers and development teams.

    Agency Ask for help
    Ask Artisan Build for help