Not every useful AI workflow looks like an autonomous agent, a chatbot, or a complex orchestration system. Sometimes the most interesting change is much smaller: a plain text file, a Bash script, and a safer way to deploy code.
That sounds modest, but it points at something I keep running into in real work. AI does not only change the way code gets written. It changes the tools that need to exist around the code. This article is about one of those tools.
A note before I start: the script I describe here is a simplified version of what I actually run. The production setup has more validation, more states, and more edge-case handling than I can fit into a readable article. I have stripped it down to the parts that carry the idea. Treat the snippets as illustrative of the design, not as the full implementation.
What you will learn
- Why AI-assisted development changes the tools around the code, not only the code itself.
- Why “what changed” and “what should be deployed” are two different lists, and why conflating them is risky.
- How a deployment queue turns an in-the-head decision into a reviewable artifact.
- Why exclusion rules must be an upload-time filter rather than a matter of discipline — and why that distinction is what makes the guardrail deterministic.
- How to design a safe handoff between an AI assistant and a human operator in a constrained, legacy environment.
The context, and the constraint that shapes everything
The application is a Laravel project hosted on shared infrastructure, where deployment still happens over FTPS. It is not a containerized environment, not a clean CI/CD pipeline, not a Kubernetes cluster, not a greenfield project. It is the kind of operational environment that exists everywhere in real businesses: useful, imperfect, constrained, and still critical.
That single constraint — FTPS on shared hosting — is worth stating plainly, because it is the reason most of the “obvious” answers do not apply. There is no shell on the server, so rsync is not available. There is no Git on the server, so I cannot diff against a deployed commit remotely or run a git pull deploy hook. There is no way to run a build step or a post-receive hook on the host. The only reliable channel is file transfer over FTPS.
So the usual reflexes — rsync --exclude, deploy-on-push, a server-side git diff — are off the table, not because they are bad, but because the environment does not offer the primitives they depend on. Whatever I build has to assume nothing more than “I can transfer and delete files over FTPS.” This is also a very concrete instance of the sandbox problem: the AI assistant operating in this workspace cannot perceive the host’s limitations on its own, so any tool I build has to encode those constraints explicitly rather than assume the model will rediscover them.
Within that constraint, the traditional workflow was fragile. A graphical FTP client such as FileZilla meant remembering which files had changed, dragging them to the server by hand, avoiding user-uploaded documents, avoiding runtime directories, avoiding configuration files, and hoping nothing important had been forgotten. That is not just inefficient. It is risky.
The application holds user-uploaded documents, runtime files, logs, cache directories, local configuration, and environment-specific secrets. Some files must be deployed. Some must never be deployed. Some exist locally but should not exist in production at all. And some files deleted locally need to be removed from the server explicitly.
A full synchronization is not a safe answer here either. The site holds more than a gigabyte of user-uploaded documents that obviously should not be re-uploaded on every deploy, and a recursive comparison across thousands of vendor files over FTPS takes an unreasonable amount of time. So full sync is both slow and dangerous, and manual FTP leans too hard on memory — which is one of the least reliable components in any deployment process.
The solution was not a sophisticated platform. It was a small Bash script built around one operational idea: a deployment queue.
The problem with “just upload the changed files”
For a small change, manual deployment looks deceptively acceptable. You edit a Blade view, change a route, maybe touch a controller, open the FTP client, upload the files. It feels faster than setting up a process.
The problem is that it does not scale even at a very small level of complexity. After a few hours of work, the list of changed files is no longer obvious. Some changes are tracked by Git, some files are generated, some are local-only, some are configuration that must not be touched, some were changed during debugging and should not ship, and some files deleted locally need to be deleted remotely — which a plain upload does not handle.
Git helps, but it does not answer the deployment question directly. git status tells me what changed in the working tree. It does not tell me what should go to the server. The two lists are related but not identical: a Git change can be irrelevant to production, a production file may need to be uploaded even if it is untracked, and a locally deleted file stays on the server unless the deployment process explicitly removes it.
Deployment is not a technical transfer of bytes. It is an operational decision about which files are safe, necessary, and intended to move from the workspace to production. In a manual FTP workflow, that decision lives almost entirely in the developer’s head. That is the part that had to change.
A queue instead of a guess
The deployment queue is a local file, ignored by Git, where each line represents one deployment action. A plain path means the file should be uploaded. A line beginning with - (a dash followed by a space, then the path) means the file should be deleted from the server. Lines beginning with # are comments and are ignored.
A simplified queue looks like this:
app/resources/views/admin/pdf.blade.php
app/routes/web.php
- app/legacy/file_to_remove.php
# the line above deletes a file on the server; this line is ignored
The script supports a handful of commands — checking the FTPS connection, a dry run, a broader incremental sync, resetting the queue, and uploading from the queue — but the queue mode is the core.
When the queue command runs, the script reads the file, separates uploads from deletions, prints both lists clearly with file sizes where relevant, asks for a single confirmation, and then performs the deployment. Uploads go one file at a time, each transfer verified against the response and the number of bytes transferred. Deletions are issued explicitly over FTP. The queue is cleared only if the whole operation succeeds; if anything fails, the queue stays intact and the operation can be retried.
This is not a glamorous architecture. It is a practical one. The script does not scan the whole server, does not infer everything from timestamps, and does not try to be a universal deployment system. It focuses on the actual constraint: deploying a small, known set of files safely over FTPS where full sync is slow and risky. That narrowness is the point. Good operational tools often become useful precisely because they refuse to solve the wrong problem.
The exclusion layer is a filter, not a request
The queue alone would not be safe enough. A tool that makes it easy to upload files also makes it easy to upload the wrong files, and in this environment that would be unacceptable. So the script carries a set of exclusion patterns: user uploads, runtime logs, Laravel cache and session directories, local configuration, secrets, Git metadata, test files, local AI-instruction files, and other non-production artifacts.
Here is the part that matters most, and the part I want to be precise about, because the whole safety argument rests on it.
The exclusion rules are applied at upload time, and they do not trust the queue. They are not advice to the person (or the assistant) writing the queue file. They are a filter that runs at the moment of transfer. If an excluded path ends up in the queue — whether a distracted human typed it, or the AI assistant appended it after editing something it should not have shipped — the upload step drops it before anything leaves the machine. The queue expresses intent; the exclusion filter decides what is actually permitted to move.
This distinction is the difference between a guardrail and a guideline.
A disciplinary safeguard sounds like: “be careful not to put the uploads directory in the queue.” That depends on everyone, every time, being careful. A deterministic safeguard sounds like: “even if the uploads directory is in the queue, the upload step will refuse to send it.” The second one does not degrade when attention does. It does not depend on the queue’s author being correct, which is exactly what you want once one of those authors is an AI assistant that edits files on its own.
So the safety property is structural, not behavioral. The queue can be wrong; the deploy still cannot send an excluded file. In the real system this is reinforced by additional checks — the confirmation step, per-file transfer verification, and an explicit failure state that preserves the queue — but the exclusion filter is the load-bearing one, and it is the one that holds even if everything upstream of it is careless. This is the same logic behind verifying AI-written code after the first commit — the safety property cannot live inside the agent’s good intentions, it has to live in a layer the agent does not control.
In a real deployment process, reliability comes from layers: explicit intent, confirmation, validation, exclusions, retry behavior, and clear failure states. The queue provides intent. The script provides execution. The exclusion filter provides the guardrail that does not rely on anyone being careful.
Where AI enters the workflow
The most interesting thing about this system is not that it is written in Bash. It is that it was designed for a workspace shared with an AI coding assistant.
When a human edits code by hand, maintaining a deployment queue tends to rot. The developer has to remember not only what changed but to write each relevant path into a separate file, and that discipline decays: the queue becomes incomplete, then unreliable, then ignored.
With an AI assistant, the equation flips. The same actor that modifies the code can record what it touched. After editing a Blade view, a route, or a controller, it appends the path to the queue. If it removes a file, it adds a delete line. If it touches a local-only file, the exclusion filter prevents that file from shipping regardless of what the assistant wrote — which is the point of the previous section.
This creates a structured handoff. The AI does not deploy. It does not decide alone what reaches production, and it does not bypass review. It leaves behind a concrete operational trace: these are the files that may need to move. The human then reviews the queue, checks the list, confirms the batch, and runs the deploy. The AI is good at tracking the mechanical consequences of its own edits; the human stays responsible for judgment, approval, and production risk. That is a much better division of labor than handing the assistant more autonomy. It is the same shape as delegating implementation work between models: the actor with the most context owns the decisions that require judgment, while the actor with the most throughput handles the mechanical follow-through inside well-defined boundaries.
It also matters that the trace is a file rather than a chat message. A common mistake in applied AI is to treat the conversation as the workflow: the assistant writes code, says what it changed, the human reads the message and acts on it. That works for trivial cases, but chat is not state, not an audit trail, not a deployment plan, and not a reliable operational interface. A queue file is primitive by comparison, but it can be inspected, reset, version-excluded, processed by a script, retried after a failure, and it survives the end of a session. It turns “I changed these files” into something the deployment system can actually act on.
The useful general move is exactly that translation: getting operational information out of conversation and into structured artifacts — files, queues, logs, checklists, schemas, tickets, test cases, validation reports, command inputs. The conversation is where the work starts. It should not be where all the operational knowledge stays trapped. This is the same principle behind structuring requirements as a machine-readable PRD.json before a coding session begins, and behind AI-assisted bug intake that produces well-formed tickets instead of long support threads — in each case, the value is created by moving information from a conversational medium into a structured artifact the rest of the system can actually act on.
The broader lesson
It would be easy to call this automation, but that misses the more important point. Yes, the script automates repetitive operations — no more opening an FTP client, dragging files, and recalling what changed — and that reduces forgotten files and accidental uploads. But the deeper change is ergonomic. The workflow has been redesigned around the fact that there is now another actor in the process: not an autonomous engineer, not a replacement for the developer, but an agent that participates in the workspace and edits code. Once that is true, the tools need different interfaces. A human carries context, intent, and risk in their head; an AI assistant edits files effectively but needs a structured channel to communicate consequences. The deployment queue is that channel.
And the example is deliberately small. It needs no new platform, no agent framework, no orchestration layer, and it does not require abandoning the legacy hosting constraint. That is exactly why it is worth writing about. Most business environments do not have perfect infrastructure — they have old systems, shared hosting, partial access, manual operations, and constraints that cannot be removed on demand. Applied AI has to work there too. The same design philosophy also scales far beyond a deploy script: an AI-assisted product sourcing pipeline that reduces 300,000 supplier references to a reviewable shortlist is built from the same ingredients — deterministic structure, cached state, a structured artifact for every decision, and a human gate before anything irreversible happens — just at a different order of magnitude.
So the interesting question is usually not “how do we build the most advanced AI system possible?” It is “where does the existing workflow depend too much on memory, repetition, or informal coordination?” Here, the fragile point was deployment memory. The assistant could change code quickly, but the deploy still depended on the human remembering every file that needed to move. The queue removed that dependency, and the exclusion filter made the result safe regardless of who or what populated it.
Conclusion
The deployment queue is a small tool, but it carries a larger principle. AI-assisted development is not only about generating code. It is about redesigning the operational interfaces around code: what gets recorded, what gets reviewed, what gets executed, and what stays protected.
The AI changes the code. The queue records the operational consequence. The exclusion filter decides what is actually allowed to move. The human reviews the intent and confirms. The script performs the deployment safely.
A Bash script and a text file may not look like an AI system. In this case they are part of one — and in applied AI, these small adjustments to how real work happens often matter more than the impressive ones. They are also a small but concrete example of software behaving as a living structure rather than a passive archive: the queue is not a record of past activity, it is an instrument the workspace uses to project the next safe action into the world.