Guide
A Comprehensive Agent's Guide to Laravel CloudRun 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 baremigrate:statusfails withcommand not found, and the result still sayscommand.success. - The environment is a positional argument. The global
--envoption 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. outputcan contain raw control characters. Parse with a lenient decoder, such as Python'sjson.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
Processfacade, remember its default timeout is 60 seconds. --no-monitorreturns as soon as the command is submitted, with exit status0. That proves nothing about the result. Use it only if you then pollcloud command:get <id> --json -n --fields=status,exitCode,outputuntil 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
--codeis required when you are non-interactive.--timeoutdefaults 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.