Local Apps Script development with clasp
Develop Google Apps Script locally with clasp (@google/clasp) and Git to clone a bound project, inspect upload files, and push verified revisions.
Assemble a local bound-script project, predict which changes reach Apps Script, review and upload an approved revision, and recover without overwriting another maintainer's work.
Develop the request summary on your computer, record a reviewable revision in Git, and upload only its Apps Script source and manifest to a disposable test project. Then change one thing locally and one thing in the spreadsheet to see which state each operation affects.
The summary reads two fictional requests and logs counts. It does not write cells, send messages, install triggers, or need a deployment. Keeping the program small makes the development workflow visible: a correct count does not prove that you uploaded the intended files to the intended project.
Know which copy you are changing
A local folder is not an Apps Script runtime. Node.js runs clasp on your computer; Google runs the uploaded .gs code on Apps Script. Installing Node.js and clasp does not make SpreadsheetApp available on your computer.
| State | What changes it | What does not change it |
|---|---|---|
| Local working files | Your editor, a file copy, or a clasp pull into that directory | Editing cells in Sheets |
| Local Git history | An explicit Git commit | Saving a file or running clasp |
| Remote project head | Saving in the Apps Script editor or a clasp push | A local commit, branch, or tag |
| Numbered Apps Script version | Creating an immutable snapshot of remote head | Editing local files after the snapshot |
| Versioned deployment | Creating a deployment or updating its selected version | A later push to head by itself |
| Spreadsheet contents | A person's cell edit or code that writes cells | Committing or uploading this read-only summary |
Remote head matters even without a public deployment. Google's content-update contract says head is used by editor executions and triggers, among other development contexts. A push can therefore change the next scheduled execution of an existing project. Use a new test spreadsheet with no triggers, not an active work tracker.
A Git branch does not select a Google account, create a test project, or isolate a remote branch. Two local branches whose .clasp.json files contain the same Script ID still upload to the same project.
Prepare the fictional spreadsheet and its bound script
You need an account authorized to create a test spreadsheet and edit its bound script, a local editor, Git, and a maintained Node.js release with npm. Use Node.js 22 or 24 for this exercise. The pinned clasp package requires Node.js 20 or newer, but do not stay on a Node.js release that is no longer supported. The terminal examples use a POSIX-style shell, such as macOS Terminal or Git Bash on Windows.
Before clasp login, confirm that the intended account can enable the Google Apps Script API and authorize clasp. On a Google Workspace domain, an administrator can restrict the Apps Script API or third-party OAuth applications even when you can open the script editor.
Create a new blank Google spreadsheet named
Local request summary - test. Do not copy a production spreadsheet with hidden data or existing automation.Rename its first sheet to
Requests.Download
requests.csv. Paste its contents intoA1, select column A, and choose Data > Split text to columns with Comma as the separator. Verify the four headers inA1:D1and the two request rows inA2:D3.From that spreadsheet, open Extensions > Apps Script. Name the project
Local request summary - test. Leave its default source unchanged for now.Open Project Settings and copy the Script ID into a private local note. Check the browser account and spreadsheet title before copying it. Do not put the ID into the source code or a Git commit.
Request,Owner,Status,Notes
Confirm room booking,Alex,Open,Check accessibility requirements
Prepare agenda,Sam,In progress,Collect topics before Friday
Request, Owner, Status, and Notes are the column names. Alex and Sam are fictional. The Requests sheet is the runtime input; the CSV is only the reproducible starting fixture. Uploading a CSV would not import it into Sheets.
Keep these identifiers distinct:
| Identifier | Where you obtain it | What it selects |
|---|---|---|
| Spreadsheet file ID | The spreadsheet URL, between /d/ and /edit | The containing Drive file |
| Script ID | The bound project's Project Settings | The script project clasp reads or replaces |
| Deployment ID | Deploy > Manage deployments, when a deployment exists | A deployment within a script project |
| Google Cloud project ID or number | The associated Cloud project's settings | Cloud configuration, not the spreadsheet or script |
The Script ID is not an access token, but it identifies infrastructure. Keep real target IDs out of this sample repository. Changing a local scriptId does not rebind a script to another spreadsheet; it makes clasp address another script.
Creating the bound script through Extensions > Apps Script also makes its container unambiguous. Do not substitute an old clasp create example with --type sheets --parentId ...: in clasp 3.4.1's creation command, the sheets branch creates a new spreadsheet rather than selecting the supplied parent. This chapter uses the already-created bound project.
Assemble the local project before connecting it
Create a fresh directory outside any existing repository or Apps Script checkout. Do not reuse an existing folder:
mkdir local-request-summary
cd local-request-summary
mkdir src fixtures remote-snapshots Download the complete Code.gs, appsscript.json, and requests.csv files from the request-summary example. Save them as src/Code.gs, src/appsscript.json, and fixtures/requests.csv. Download the four local-development configuration files into the new directory's root, preserving their names, including the leading dots. If a browser adds .txt to a download, remove that suffix when saving it.
The two example groups have different jobs. local-request-summary supplies the complete Apps Script program and input. local-development supplies local tooling; it is not a second Apps Script program and adds no service, runtime helper, or remote entrypoint.
Your authored files should be:
local-request-summary/
.claspignore
.gitignore
clasp.example.json
package.json
src/
Code.gs
appsscript.json
fixtures/
requests.csv
remote-snapshots/ The source reads displayed cell values, skips the header, keeps rows with a nonempty first column, and counts the exact status Open. It does not trim or normalize statuses. Use the supplied spelling and two-row fixture before experimenting with other inputs.
function logRequestSummary() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Requests');
if (!sheet) throw new Error('Create a sheet named Requests before running this function.');
const values = sheet.getDataRange().getDisplayValues();
if (values.length < 2) throw new Error('Add a header row and at least one request row.');
const rows = values.slice(1).filter((row) => row[0]);
const openCount = rows.filter((row) => row[2] === 'Open').length;
Logger.log('Requests: ' + rows.length + ', open: ' + openCount + '.');
}
{
"timeZone": "Etc/UTC",
"runtimeVersion": "V8",
"oauthScopes": [
"https://www.googleapis.com/auth/spreadsheets.currentonly"
]
}
Keep the manifest's V8, Etc/UTC, and spreadsheets.currentonly scope. That scope limits spreadsheet access to the current file; it is not a read-only permission. The code's lack of write calls makes this example read-only. The manifest does not grant clasp permission to manage source files.
Pin the tool, not a global installation
{
"name": "book-local-request-summary",
"version": "1.0.0",
"private": true,
"engines": {
"node": ">=22"
},
"scripts": {
"clasp": "clasp --user book-test",
"target": "clasp --user book-test --project .clasp.json --ignore .claspignore"
},
"devDependencies": {
"@google/clasp": "3.4.1"
}
}
This package pins @google/clasp to 3.4.1, rather than a floating version or whichever global executable happens to be installed. That version and its Node.js requirement come from the published npm metadata; the command examples follow its published source revision. Recheck the command help and release notes before upgrading the pin.
npm run clasp -- ... selects the named clasp account book-test. npm run target -- ... additionally selects the local .clasp.json and .claspignore. The name book-test is a local credential label, not a sandbox or a restriction on that account's access. The two scripts have no automatic push or deployment hooks.
Before installing, review the package file. Installation downloads dependencies and writes node_modules and a lockfile on your computer; it does not connect to Apps Script. Run these commands from the new directory:
node --version
npm --version
git --version
npm install --include=dev --ignore-scripts
npm run clasp -- --version The expected clasp version is 3.4.1. Stop if the command is missing or reports another version; do not substitute an unpinned npx download or a global executable. --ignore-scripts skips dependency lifecycle scripts during installation. It does not prevent a later, explicit npm run clasp from running clasp.
The first installation resolves transitive dependencies and creates package-lock.json; the direct clasp pin alone does not freeze that entire dependency graph. Review and commit the generated lockfile. On a later clean checkout, use npm ci --include=dev --ignore-scripts with that committed lockfile. npm documents that ci requires a matching lockfile, does not update it, and replaces an existing node_modules directory.
Separate uploads from version control
**/**
!Code.gs
!appsscript.json
clasp evaluates these patterns relative to rootDir, which will be src. The first pattern excludes files, and the two negated patterns admit only Code.gs and appsscript.json. Do not change them to !src/Code.gs: src is already the upload root. The CSV, npm files, credentials, and snapshots stay outside it.
This is an upload allowlist, not a Git ignore file. If you later add Menu.gs or an HTML file, it will not upload until you update and review the allowlist. Conversely, adding a file to Git does not make clasp upload it.
# Dependencies and generated or temporary output.
node_modules/
/build/
/dist/
/coverage/
/remote-snapshots/
/tmp/
*.log
*.tmp
*.tsbuildinfo
.DS_Store
# Live targets and account credentials are local, not source.
.clasp.json
.clasp.*.json
.clasprc.json
.clasprc.*.json
.env
.env.*
.npmrc
credentials.json
credentials-*.json
client_secret*.json
service-account*.json
*.pem
*.key
The Git rules exclude live target configurations, common credential filenames, local environment files, npm credentials, downloaded dependencies, snapshots, and temporary output. clasp.example.json is an inert, committable template; .clasp.json is the ignored live target.
Ignore patterns do not inspect file contents and cannot recognize every possible credential filename. Never copy tokens or production data into an allowed file. Git also does not stop tracking an already-tracked file because you add an ignore rule later. This exercise starts with a clean repository; it is not a procedure for cleaning secrets from an existing history.
Initialize Git and review what will be committed:
git init
git check-ignore -v .clasp.json .clasprc.json credentials.json remote-snapshots/probe.json node_modules/probe.js
git status --short
git add .gitignore .claspignore clasp.example.json package.json package-lock.json src/Code.gs src/appsscript.json fixtures/requests.csv
git diff --cached --stat
git diff --cached git check-ignore should show the relevant rules for those paths; the probe files need not exist. The staged diff should contain only the listed source, fixture, configuration template, ignore files, and package files. Review the lockfile too: it should not contain private registry credentials or a private filesystem dependency. Do not use git add . to bypass that review.
Once that diff is correct, record the local baseline:
git commit -m "Add read-only request summary baseline"
git tag lesson-baseline This commit and tag affect only local Git history. They do not upload source, authorize the script, save a numbered Apps Script version, or create a deployment. No Git remote or repository publication is needed for this exercise.
Select the account and the exact test target
Using your test account, open Apps Script user settings (script.google.com/home/usersettings) and enable Google Apps Script API. Google's API authorization guide explains that this account-level setting permits separately authorized applications to manage script projects; it is not limited to this one example. Each application still needs authorization.
Next, authorize clasp and inspect the selected account:
npm run clasp -- login
npm run clasp -- show-authorized-user The login opens an OAuth flow. Select the account that owns the test spreadsheet and review the requested permissions before consenting: clasp's project-management permissions are broader than the summary's current-spreadsheet runtime scope.
show-authorized-user reports the selected identity and OAuth client information. Check the email locally; do not paste account output or credential files into a public issue. The default credential store is ~/.clasprc.json, outside this repository. It contains tokens and must not be copied into source control. The sample also ignores common in-repository credential names as a second precaution. It does not require a custom OAuth client, service-account key, or a project-specific credential file.
Create the live configuration from the template:
{
"scriptId": "REPLACE_WITH_TEST_SCRIPT_ID",
"rootDir": "src",
"scriptExtensions": [".gs"]
}
cp clasp.example.json .clasp.json In your editor, replace only REPLACE_WITH_TEST_SCRIPT_ID in .clasp.json with the Script ID from the bound test project. Leave clasp.example.json unchanged. Keep rootDir as src; scriptExtensions makes downloaded server files use .gs and limits this local project to that script extension.
Compare the saved ID with Project Settings again. Open the script from the test spreadsheet to confirm the binding, account, and project title. show-authorized-user confirms an account, not a target. .clasp.json selects a target, not an account. Both checks are necessary.
The target npm script makes the configuration and ignore file explicit on each command. It is still a convention, not a permissions boundary: editing that ignored configuration or overriding command options can select something else. Keep this checkout for the test project rather than swapping test and production IDs.
Save remote head without overwriting local work
Before the first push, retain the current remote files in a new, ignored snapshot directory. A clasp pull writes local files; it does not merge with them and does not ask Git whether they contain uncommitted work. Do not pull straight into the populated src directory.
From the repository root:
mkdir remote-snapshots/before-first-push
cp .clasp.json remote-snapshots/before-first-push/.clasp.json Open the copied configuration and change its rootDir from src to .. Keep its Script ID and scriptExtensions unchanged. Do not edit the root configuration. The copied configuration now makes this new snapshot directory the download destination.
npm run clasp -- --project remote-snapshots/before-first-push/.clasp.json pull This command reads the selected remote head and writes its files into the snapshot directory. It does not upload local source or run the script. The pull implementation can also delete local files when given --deleteUnusedFiles; do not add that flag or --force here.
Inspect every downloaded file in your editor. For the new project, confirm that you are replacing only its known default source and manifest. If you find another person's code, additional files, or unfamiliar configuration, stop before pushing and resolve the target or ownership discrepancy. An upload allowlist would not preserve those remote files.
For the default Code.gs and manifest, compare the snapshot with the local program:
git diff --no-index -- remote-snapshots/before-first-push/Code.gs src/Code.gs
git diff --no-index -- remote-snapshots/before-first-push/appsscript.json src/appsscript.json These comparisons do not stage or upload anything. git diff --no-index returns exit status 1 when it finds differences, which is expected here. If the remote project has different filenames, inspect them too; these two paths may not cover the whole project.
A snapshot is a copy of code and its manifest, not a backup of spreadsheet contents, properties, installed triggers, permissions, or deployments. Keep it private. Its copied configuration still points at the same remote project: never push from a snapshot directory as a shortcut to recovery.
When you actually need clone
clasp clone-script copies an existing script's source into a local directory and writes target configuration. It does not copy the spreadsheet, create a new remote script, or remove other editors' access.
If starting from an existing test script instead of the sample source, clone into a separate empty directory. To inspect that operation without overwriting the exercise checkout, run this optional command form from the repository root after replacing TEST_SCRIPT_ID with the verified ID:
mkdir remote-snapshots/clone-study &&
(
cd remote-snapshots/clone-study &&
node ../../node_modules/@google/clasp/build/src/index.js --user book-test --project . clone-script TEST_SCRIPT_ID --rootDir src
) This calls the installed 3.4.1 package's executable, not a global clasp. The explicit --project . selects the new directory rather than searching upward for the exercise's existing configuration. The parentheses keep the directory change inside a subshell; afterward your terminal remains at the repository root. The leading mkdir must succeed, so do not reuse an existing snapshot directory.
The 3.4.1 clone command downloads source into that directory's src and writes its .clasp.json. It does not replace the exercise's working files. Inspect the result without pushing from this snapshot.
With default settings, cloned server files use .js. Review the generated configuration and downloaded filenames together before adopting this chapter's .gs-only allowlist; do not leave both Code.js and Code.gs representing the same remote file. A clone is an alternative starting workflow, not an extra step in this exercise.
Review the upload and run the baseline
Return to the root configuration and inspect the upload candidates:
npm run target -- show-file-status
git status --short
git diff -- src/Code.gs src/appsscript.json .claspignore The clasp upload list must contain exactly src/Code.gs and src/appsscript.json; order and display formatting are not significant. fixtures/requests.csv, package.json, node_modules, .clasp.json, credentials, and snapshots must not be upload candidates. show-file-status calls its candidates "tracked," but that is clasp's selection, not Git's index.
The status command examines local selection. It does not verify Google ownership, approve manifest scopes, prove remote equality, or reserve the project against a collaborator's edit.
Confirm that nobody is editing the test project. When clasp uploads, it replaces the remote content set; it does not merge your files with someone else's remote additions. --force bypasses manifest confirmation rather than resolving conflicts; do not use it or --watch here.
npm run target -- push If clasp prompts to overwrite the manifest, compare the already-reviewed manifest and confirm only that intended change. A skipped prompt or an "already up to date" message is not execution evidence. In particular, do not rely on the command's change detection to prove that unexpected remote-only files are absent.
After the upload, reopen the Apps Script editor from the test spreadsheet and inspect Code.gs. In Project Settings, enable Show "appsscript.json" manifest file in editor, return to Editor, and inspect the uploaded manifest. Do not make a competing editor edit while you are checking the upload.
Select logRequestSummary and click Run. On the first run, review and approve the current-spreadsheet authorization using the intended account. This runtime authorization is separate from clasp login. Inspect the Execution log, and use Executions if you need the run's status and error details.
For the unchanged fixture, expect:
Requests: 2, open: 1. The script leaves A1:D3 unchanged. No clasp run-function, API executable, web endpoint, or trigger is required; the editor supplies the bound-spreadsheet context this function uses.
Predict, change, and observe two different states
First distinguish a fixture edit from a spreadsheet edit:
Predict whether changing
fixtures/requests.csvwould change the next editor run without importing it. It would not: the function reads the sheet, not the local CSV. Leave the committed fixture unchanged.In the test spreadsheet only, change
C3fromIn progresstoOpen. Do not edit source or push. Run the same function in the editor. ExpectRequests: 2, open: 2..Restore
C3toIn progress, rerun, and expect the baseline count again. Git history and remote source have not changed during these cell edits.
Next make a real source revision. Start with the restored fixture and a clean working tree:
git switch -c count-active In local src/Code.gs, change the status predicate from row[2] === 'Open' to row[2] === 'Open' || row[2] === 'In progress'. Rename openCount to activeCount at both occurrences, and change the log label from , open: to , active: . Do not change the function name, sheet access, manifest, or row filter.
Predict the observations before uploading:
Running the editor now should still log
Requests: 2, open: 1.. Saving and committing the local file do not change remote head.After the reviewed upload, the same fixture should log
Requests: 2, active: 2.because both statuses meet the revised condition.After that upload, changing only
C3toClosedshould giveRequests: 2, active: 1.. RestoreIn progressafterward. This checks that the condition excludes another status, not only that the log label changed.
Review and commit the revision locally:
git diff -- src/Code.gs
git add src/Code.gs
git diff --cached
git commit -m "Count open and in-progress requests" Do not push as soon as Git is clean. Repeat the remote snapshot procedure with a new directory, such as remote-snapshots/before-active-count, and inspect remote head. It should still match the baseline. If another person has changed it, use the next section rather than overwriting it.
When the account, target, remote baseline, local diff, and two-file upload list match, run npm run target -- push and perform the predicted editor runs.
Pull and recover without discarding another person's work
For ongoing work, choose one source-editing workflow and an upload owner for a given remote project. Prefer local Git-reviewed changes; if someone must edit in the Apps Script editor, stop uploads until those changes have been recovered and reviewed. There is no Git-style remote branch or automatic three-way merge inside clasp.
To recover an editor change, use a new snapshot directory and copied configuration as above, with rootDir set to . in that copy. A plain pull downloads current head. Compare all downloaded files with your working source, identify who made the differences, and incorporate the agreed changes into your local branch. Review manifest changes and newly added files as carefully as code.
Do not exclude a collaborator's new file and assume clasp will leave it on the server. Either bring the file into the local project and extend the upload allowlist, or agree that its removal is intended. Preserve both versions while resolving a conflict; do not solve it with push --force, an in-place pull, or a broad Git reset.
A fresh snapshot reduces uncertainty but does not prevent an edit between inspection and upload. Coordinate an editing pause with the other maintainer.
Restore this exercise's local baseline
After the exercise revision has been committed, check that there are no new uncommitted changes to src/Code.gs. The next command replaces only that local file with the tagged baseline. It does not erase the committed revision:
git status --short
git restore --source=lesson-baseline -- src/Code.gs
git diff -- src/Code.gs
git add src/Code.gs
git diff --cached
git commit -m "Restore the request summary baseline" Remote head still has the active-count revision until you upload the restoration. Repeat the account, target, snapshot, diff, and upload review before that push, then run logRequestSummary against the restored fixture and check for Requests: 2, open: 1.. Restoring code does not restore cells that someone changed.
For recovery from an existing numbered Apps Script version, first use npm run target -- list-versions to identify the version. In a new snapshot directory with its own copied configuration, the download command form is:
npm run clasp -- --project remote-snapshots/recovery/.clasp.json pull --versionNumber KNOWN_VERSION_NUMBER Create remote-snapshots/recovery and its configuration using the same procedure before running that command, and replace KNOWN_VERSION_NUMBER with the verified numeric version. This downloads historical code into the snapshot; it does not roll back remote head or a deployment. Review and integrate the desired source into the working project before uploading.
If a push times out or the terminal loses its response, the remote outcome may be unknown. Do not assume that failure to receive success means no files changed. Obtain a new remote snapshot, compare it with the intended upload, and check actual executions before deciding whether another upload is needed.
Keep versions separate from deployments
A remote Apps Script project has one head deployment that follows its most recently saved code. An Apps Script version is an immutable snapshot of that remote project's code. A versioned deployment selects a version to expose as an application. Google's deployment guide distinguishes head deployments from versioned deployments.
This read-only summary needs neither a numbered version nor a versioned deployment to run in its bound editor. Do not add a web-app or API manifest configuration only to make a deployment command succeed. In particular, a web app or Apps Script API execution does not supply the same bound-container methods as an editor run.
When maintaining a deployable test application, follow this release sequence:
Select that application's test account and target, review the complete source and manifest, coordinate remote ownership, and upload the reviewed code.
Verify the intended execution path. A successful editor run does not prove web-app identity, access settings, event behavior, or external-client access.
Create a numbered version of that verified remote head.
Review deployment type, execution identity, audience, scopes, ownership, and rollback choice. Then create a deployment, or update an existing one to the selected number.
The following are clasp 3.4.1 command forms, not commands to run against this summary project. Use that other application's equivalent explicit-target npm script. Replace the uppercase tokens with verified values from that project; do not guess a version or deployment ID:
npm run target -- create-version "Reviewed test release"
npm run target -- list-versions
npm run target -- list-deployments create-version changes remote version history; it does not upload pending local edits. Record the returned version number alongside the reviewed Git commit. A Git hash and an Apps Script version number identify different snapshots, so do not equate them without checking what was uploaded.
For a new deployment:
npm run target -- create-deployment --versionNumber APPROVED_VERSION_NUMBER --description "Approved test release" For an existing deployment:
npm run target -- update-deployment EXISTING_DEPLOYMENT_ID --versionNumber APPROVED_VERSION_NUMBER --description "Approved test revision"
npm run target -- list-deployments Creating a deployment creates a new deployment ID; updating one changes its selected version while retaining its ID. Passing an explicit number avoids the 3.4.1 deployment implementation's implicit creation of another version when no number is supplied. Inspect the resulting deployment and exercise its actual consumer path before calling the release successful.
A rollback updates that same deployment to a previously verified compatible version:
npm run target -- update-deployment EXISTING_DEPLOYMENT_ID --versionNumber PREVIOUS_VERIFIED_VERSION_NUMBER --description "Approved rollback" This does not restore remote head, spreadsheet data, properties, permissions, or external effects. Older code may no longer understand current data. Confirm that compatibility and the deployment's actual selected version before and after the change; if the update response is lost, inspect deployment state before issuing another mutation.
Add TypeScript or libraries only when needed
This project has no compilation step. Apps Script's V8 runtime is neither Node.js nor a browser: it does not supply Node module loading, browser window, or browser fetch. clasp runs locally on Node.js, but that does not make Node dependencies available inside uploaded code.
If a larger project uses TypeScript, supply Apps Script declarations, check the authored source, and configure a compiler or bundler to emit compatible JavaScript. clasp 3.x does not transpile TypeScript. Keep source separate from generated output, point rootDir at the reviewed output, and update the upload allowlist accordingly. Inspect emitted files for unresolved imports and Node or browser assumptions before uploading. Do not apply those changes to this no-build sample without also changing its instructions.
An Apps Script library is a script project with an identifier, access requirements, and selected version, not an npm package. Use one when several projects need a maintained interface; include its version, scopes, shared resources, and owner in review. Libraries add overhead, so a tiny single-project helper need not become a separate release process.
Neither local typing nor a library changes Apps Script execution limits. Review current Apps Script quotas and any external provider's limits before scheduling larger workloads. Use the execution and recovery techniques in debugging and quotas; do not treat a successful upload as an operational test.
Troubleshoot by locating the wrong state
| What you observe | Check before changing anything else |
|---|---|
| clasp reports an unknown command or version | Confirm the local package and npm run clasp -- --version. Read the pinned command's --help; do not mix older aliases or positional arguments into a different release. |
| Login succeeds but project access fails | Confirm the selected account, account-level Apps Script API access, project edit rights, and organizational policy. Runtime spreadsheet authorization is a separate step. |
| clasp selects the wrong project | Stop before any write. Compare .clasp.json with the bound test project's Script ID, not the spreadsheet ID or title alone. |
| The upload list is empty or has an extra file | Check rootDir, exact filenames, extensions, and root-relative .claspignore patterns. Do not widen the allowlist until the intended complete remote content is known. |
| A manifest push is skipped | Inspect the intended scope/configuration change and the interactive confirmation. Do not add --force to hide an unexplained refusal. |
| The log still shows the old label | Distinguish a local-only edit from uploaded head; inspect the actual remote source, function selection, account, and execution record. |
The script cannot find Requests or an active spreadsheet | Confirm the sheet name and bound editor context. Do not run this function through Node.js or assume an API execution has an active container. |
| A local fixture edit has no effect | The runtime reads spreadsheet cells, not the CSV. Import only fictional data into the intended test file. |
| A collaborator's file is missing locally | Pull into a fresh snapshot and reconcile before uploading. A successful local commit gives no permission to erase that remote file. |
| Git shows a credential or live target as tracked | Stop before committing or sharing. Ignore rules are not retroactive. If a credential was exposed, revoke or rotate it and follow the repository's incident procedure rather than relying on deletion from the latest file. |
Leave the exercise in a known state
Restore the test sheet's original C3 status and retain any observations you need without copying tokens or real data into the repository.
Keep the source, lockfile, and Git history if you plan to return. Close terminals before removing disposable local dependencies or snapshot directories through your file manager. Do not delete the shared ~/.clasprc.json file, which may hold other clasp accounts.
If the book-test credential is no longer needed, first confirm its identity, then remove that named local credential:
npm run clasp -- show-authorized-user
npm run clasp -- logout In this clasp release, logout removes the stored credential for that label; it does not revoke Google-side OAuth access. Review the Google account's third-party access separately if you want to revoke it. Turning off account-level Apps Script API access can affect other authorized tools, so leave it enabled if other tools use it.
Removing the local folder does not remove the remote script or spreadsheet. Trash the disposable test spreadsheet when you no longer need it.
Next, test your extracted helpers locally and run smoke tests in a staging sheet; keep Reference and glossary bookmarked for everyday lookup.
Project files
Complete source files for this chapter’s examples.
Log a request summary from a local Apps Script project
Code.gs
Download filefunction logRequestSummary() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Requests');
if (!sheet) throw new Error('Create a sheet named Requests before running this function.');
const values = sheet.getDataRange().getDisplayValues();
if (values.length < 2) throw new Error('Add a header row and at least one request row.');
const rows = values.slice(1).filter((row) => row[0]);
const openCount = rows.filter((row) => row[2] === 'Open').length;
Logger.log('Requests: ' + rows.length + ', open: ' + openCount + '.');
}
appsscript.json
Download file{
"timeZone": "Etc/UTC",
"runtimeVersion": "V8",
"oauthScopes": [
"https://www.googleapis.com/auth/spreadsheets.currentonly"
]
}
requests.csv
Download fileRequest,Owner,Status,Notes
Confirm room booking,Alex,Open,Check accessibility requirements
Prepare agenda,Sam,In progress,Collect topics before Friday
Pinned clasp and Git configuration for the request summary
package.json
Download file{
"name": "book-local-request-summary",
"version": "1.0.0",
"private": true,
"engines": {
"node": ">=22"
},
"scripts": {
"clasp": "clasp --user book-test",
"target": "clasp --user book-test --project .clasp.json --ignore .claspignore"
},
"devDependencies": {
"@google/clasp": "3.4.1"
}
}
clasp.example.json
Download file{
"scriptId": "REPLACE_WITH_TEST_SCRIPT_ID",
"rootDir": "src",
"scriptExtensions": [".gs"]
}
.claspignore
Download file**/**
!Code.gs
!appsscript.json
.gitignore
Download file# Dependencies and generated or temporary output.
node_modules/
/build/
/dist/
/coverage/
/remote-snapshots/
/tmp/
*.log
*.tmp
*.tsbuildinfo
.DS_Store
# Live targets and account credentials are local, not source.
.clasp.json
.clasp.*.json
.clasprc.json
.clasprc.*.json
.env
.env.*
.npmrc
credentials.json
credentials-*.json
client_secret*.json
service-account*.json
*.pem
*.key